From 5c059360b8f7346a7a5c9ebe6541b19dbfdd2021 Mon Sep 17 00:00:00 2001 From: Swen Gross <25036977+emphasize@users.noreply.github.com> Date: Wed, 8 Nov 2023 22:27:10 +0100 Subject: [PATCH 01/15] elevate sound media role (#201) --- ovos_utils/sound/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ovos_utils/sound/__init__.py b/ovos_utils/sound/__init__.py index 28cdc275..056b3ce0 100644 --- a/ovos_utils/sound/__init__.py +++ b/ovos_utils/sound/__init__.py @@ -17,12 +17,12 @@ def read_mycroft_config(): return dict() -# Create a custom environment to use that can be ducked by a phone role. -# This is kept separate from the normal os.environ to ensure that the TTS -# role isn't affected and that any thirdparty software launched through -# a mycroft process can select if they wish to honor this. +# Create a custom environment to use that can let duck a music role. +# This is kept separate from the normal os.environ to ensure that +# any thirdparty software launched through +# a ovos process can select if they wish to honor this. _ENVIRONMENT = deepcopy(os.environ) -_ENVIRONMENT['PULSE_PROP'] = 'media.role=music' +_ENVIRONMENT['PULSE_PROP'] = 'media.role=phone' def _get_pulse_environment(config): From 167d6c1bc379c6dec266f8c4b276eac0c927dd76 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 8 Nov 2023 21:27:23 +0000 Subject: [PATCH 02/15] Increment Version to 0.0.37a1 --- ovos_utils/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 8b1643bb..bb3b4c9c 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -2,6 +2,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 0 -VERSION_BUILD = 36 -VERSION_ALPHA = 0 +VERSION_BUILD = 37 +VERSION_ALPHA = 1 # END_VERSION_BLOCK From 3384462cd35d5d28d26f35ccbfd7d23ce5778035 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 8 Nov 2023 21:27:53 +0000 Subject: [PATCH 03/15] Update Changelog --- CHANGELOG.md | 110 ++------------------------------------------------- 1 file changed, 3 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f5d694..6b5d5df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,116 +1,12 @@ # Changelog -## [0.0.36](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.36) (2023-10-25) +## [0.0.37a1](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a1) (2023-11-08) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a12...0.0.36) - -**Closed issues:** - -- Import `EventSchedulerInterface` from ovos-bus-client [\#168](https://github.com/OpenVoiceOS/ovos-utils/issues/168) - -## [V0.0.36a12](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a12) (2023-10-25) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a11...V0.0.36a12) - -**Fixed bugs:** - -- \[GUI\] `_pages2uri` reports page not found when omitting extension [\#194](https://github.com/OpenVoiceOS/ovos-utils/issues/194) - -**Merged pull requests:** - -- Remove error log for GUI resource resolution [\#198](https://github.com/OpenVoiceOS/ovos-utils/pull/198) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.36a11](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a11) (2023-10-25) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a10...V0.0.36a11) - -**Merged pull requests:** - -- deprecate the sound utils module [\#190](https://github.com/OpenVoiceOS/ovos-utils/pull/190) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.36a10](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a10) (2023-10-19) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a9...V0.0.36a10) - -**Fixed bugs:** - -- fix/scheduled\_event message.context [\#196](https://github.com/OpenVoiceOS/ovos-utils/pull/196) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.36a9](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a9) (2023-10-12) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a8...V0.0.36a9) - -**Implemented enhancements:** - -- update FakeBus to support session [\#195](https://github.com/OpenVoiceOS/ovos-utils/pull/195) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.36a8](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a8) (2023-09-26) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a7...V0.0.36a8) - -**Implemented enhancements:** - -- Support "logging" section to allow per-service log level changes [\#189](https://github.com/OpenVoiceOS/ovos-utils/pull/189) ([lbt](https://github.com/lbt)) - -## [V0.0.36a7](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a7) (2023-09-17) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a6...V0.0.36a7) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36...0.0.37a1) **Fixed bugs:** -- hotfix/gui\_sync [\#192](https://github.com/OpenVoiceOS/ovos-utils/pull/192) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.36a6](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a6) (2023-09-13) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a5...V0.0.36a6) - -**Merged pull requests:** - -- refactor/less\_gui\_spam [\#191](https://github.com/OpenVoiceOS/ovos-utils/pull/191) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.36a5](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a5) (2023-09-05) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a4...V0.0.36a5) - -**Implemented enhancements:** - -- Only scan for executables if a good one has not yet been found [\#187](https://github.com/OpenVoiceOS/ovos-utils/pull/187) ([lbt](https://github.com/lbt)) - -## [V0.0.36a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a4) (2023-09-05) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a3...V0.0.36a4) - -**Implemented enhancements:** - -- IntentServiceInterface Tests and Minor Fixes [\#186](https://github.com/OpenVoiceOS/ovos-utils/pull/186) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.36a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a3) (2023-08-17) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a2...V0.0.36a3) - -**Fixed bugs:** - -- remove \_HACK [\#184](https://github.com/OpenVoiceOS/ovos-utils/pull/184) ([JarbasAl](https://github.com/JarbasAl)) - -**Closed issues:** - -- \_\_HACK\_preload in file\_utils.py causes circular import when launching ovos\_dinkum\_listener [\#183](https://github.com/OpenVoiceOS/ovos-utils/issues/183) - -## [V0.0.36a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a2) (2023-08-07) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36a1...V0.0.36a2) - -**Fixed bugs:** - -- HACK / speed up resolve\_ovos\_resource\_file [\#182](https://github.com/OpenVoiceOS/ovos-utils/pull/182) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.36a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.36a1) (2023-08-07) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.35...V0.0.36a1) - -**Merged pull requests:** - -- Update `events` module docs and default values [\#181](https://github.com/OpenVoiceOS/ovos-utils/pull/181) ([NeonDaniel](https://github.com/NeonDaniel)) +- elevate sound media role [\#201](https://github.com/OpenVoiceOS/ovos-utils/pull/201) ([emphasize](https://github.com/emphasize)) From f844113fc338f7a1deb3ab469f6bad5f0ca4aa12 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:19:15 +0000 Subject: [PATCH 04/15] update imports for py 3.10 compat (#202) * update imports for py 3.10 compat fix https://github.com/OpenVoiceOS/ovos-PHAL-plugin-mk1/issues/6 * Update __init__.py --- ovos_utils/enclosure/mark1/faceplate/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_utils/enclosure/mark1/faceplate/__init__.py b/ovos_utils/enclosure/mark1/faceplate/__init__.py index fc7086a3..308f7d08 100644 --- a/ovos_utils/enclosure/mark1/faceplate/__init__.py +++ b/ovos_utils/enclosure/mark1/faceplate/__init__.py @@ -4,11 +4,11 @@ from ovos_utils.messagebus import get_mycroft_bus import random from time import sleep -import collections +from collections.abc import MutableSequence import copy -class FaceplateGrid(collections.MutableSequence): +class FaceplateGrid(MutableSequence): encoded = None str_grid = None pad_char = "." From 4b887726d7877a7bfa76f7b2bbab0cbf547e2e51 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 18 Dec 2023 18:19:34 +0000 Subject: [PATCH 05/15] Increment Version to 0.0.37a2 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index bb3b4c9c..0674a6a8 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 37 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK From ef22413d7ee04170b3fe8dc4e973ddfe0476f6ba Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 18 Dec 2023 18:20:20 +0000 Subject: [PATCH 06/15] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b5d5df2..e8570468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.37a1](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a1) (2023-11-08) +## [0.0.37a2](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a2) (2023-12-18) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36...0.0.37a1) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a1...0.0.37a2) + +**Fixed bugs:** + +- update imports for py 3.10 compat [\#202](https://github.com/OpenVoiceOS/ovos-utils/pull/202) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.37a1](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a1) (2023-11-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.36...V0.0.37a1) **Fixed bugs:** From 7e5e62b88e5e5c6de04d2c560782206bbf5a6f91 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:51:02 +0000 Subject: [PATCH 07/15] deprecation warnings for 0.1.0 (#206) * deprecate skills deprecate intents deprecate mk1 Update messagebus.py Update audioservice.py Update audioservice.py Update layers.py Update api.py Update layers.py Update api.py Update api.py Update audioservice.py Update enclosure api.py * fix tests * fix tests * fix tests --- .github/workflows/build_tests.yml | 11 +- .github/workflows/unit_tests.yml | 1 + .gitignore | 1 + ovos_utils/enclosure/__init__.py | 2 + ovos_utils/enclosure/api.py | 671 ++++++----- ovos_utils/enclosure/mark1/__init__.py | 4 + ovos_utils/enclosure/mark1/eyes/__init__.py | 967 +++++++-------- .../enclosure/mark1/faceplate/__init__.py | 755 ++++++------ .../enclosure/mark1/faceplate/animations.py | 1072 ++++++++--------- .../mark1/faceplate/cellular_automaton.py | 943 +++++++-------- ovos_utils/enclosure/mark1/faceplate/icons.py | 452 +++---- ovos_utils/events.py | 34 +- ovos_utils/intents/__init__.py | 7 +- ovos_utils/intents/converse.py | 12 +- .../intents/intent_service_interface.py | 712 +++++------ ovos_utils/intents/layers.py | 304 ++--- ovos_utils/messagebus.py | 186 +-- ovos_utils/skills/api.py | 136 +-- ovos_utils/skills/audioservice.py | 493 ++++---- ovos_utils/skills/locations.py | 264 ++-- ovos_utils/skills/settings.py | 110 +- requirements/test.txt | 3 + test/unittests/test_skills.py | 2 +- 23 files changed, 3509 insertions(+), 3633 deletions(-) create mode 100644 requirements/test.txt diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index f940ea3d..15be9921 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -38,7 +38,7 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev swig libssl-dev + sudo apt install python3-dev - name: Build Source Packages run: | python setup.py sdist @@ -48,12 +48,3 @@ jobs: - name: Install package run: | pip install .[all] - - uses: pypa/gh-action-pip-audit@v1.0.7 - with: - # Ignore setuptools vulnerability we can't do much about - # Ignore requests vulnerability - # Ignore Setuptools vulnerability - ignore-vulns: | - GHSA-r9hx-vwmv-q579 - GHSA-j8r2-6x86-q33q - PYSEC-2022-43012 \ No newline at end of file diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d94b0e7e..53c1a11b 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -52,6 +52,7 @@ jobs: python -m pip install build wheel - name: Install core repo run: | + pip install -r requirements/test.txt pip install -e .[extras] - name: Install test dependencies run: | diff --git a/.gitignore b/.gitignore index 9595be50..c8ece8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dist # Created by unit tests .pytest_cache/ +/.gtm/ diff --git a/ovos_utils/enclosure/__init__.py b/ovos_utils/enclosure/__init__.py index efb4ef4b..bbebc37f 100644 --- a/ovos_utils/enclosure/__init__.py +++ b/ovos_utils/enclosure/__init__.py @@ -5,6 +5,8 @@ from typing import Optional from ovos_utils.log import LOG, deprecated +LOG.warning("ovos_utils.enclosure has been deprecated! this module will be removed in version 0.1.0") + class MycroftEnclosures(str, Enum): # TODO: Deprecate in 0.1.0 diff --git a/ovos_utils/enclosure/api.py b/ovos_utils/enclosure/api.py index 24d54683..7f48e48e 100644 --- a/ovos_utils/enclosure/api.py +++ b/ovos_utils/enclosure/api.py @@ -1,337 +1,344 @@ -from ovos_utils.messagebus import FakeMessage as Message, dig_for_message +from ovos_utils.log import LOG +LOG.warning("EnclosureApi has moved to ovos_bus_client.apis.enclosure") -class EnclosureAPI: - """ - This API is intended to be used to interface with the hardware - that is running Mycroft. It exposes all possible commands which - can be sent to a Mycroft enclosure implementation. - - Different enclosure implementations may implement this differently - and/or may ignore certain API calls completely. For example, - the eyes_color() API might be ignore on a Mycroft that uses simple - LEDs which only turn on/off, or not at all on an implementation - where there is no face at all. - """ - - def __init__(self, bus=None, skill_id=""): - self.bus = bus - self.skill_id = skill_id - - def set_bus(self, bus): - self.bus = bus - - def set_id(self, skill_id): - self.skill_id = skill_id - - def _get_source_message(self): - return dig_for_message() or \ - Message("", context={"destination": ["enclosure"], - "skill_id": self.skill_id}) - - def register(self, skill_id=""): - """Registers a skill as active. Used for speak() and speak_dialog() - to 'patch' a previous implementation. Somewhat hacky. - DEPRECATED - unused - """ - source_message = self._get_source_message() - skill_id = skill_id or self.skill_id - self.bus.emit(source_message.forward("enclosure.active_skill", - {"skill_id": skill_id})) - - def reset(self): - """The enclosure should restore itself to a started state. - Typically this would be represented by the eyes being 'open' - and the mouth reset to its default (smile or blank). - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.reset")) - - def system_reset(self): - """The enclosure hardware should reset any CPUs, etc.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.reset")) - - def system_mute(self): - """Mute (turn off) the system speaker.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.mute")) - - def system_unmute(self): - """Unmute (turn on) the system speaker.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.unmute")) - - def system_blink(self, times): - """The 'eyes' should blink the given number of times. - Args: - times (int): number of times to blink - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.system.blink", - {'times': times})) - - def eyes_on(self): - """Illuminate or show the eyes.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.on")) - - def eyes_off(self): - """Turn off or hide the eyes.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.off")) - - def eyes_blink(self, side): - """Make the eyes blink - Args: - side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.blink", - {'side': side})) - - def eyes_narrow(self): - """Make the eyes look narrow, like a squint""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.narrow")) - - def eyes_look(self, side): - """Make the eyes look to the given side - Args: - side (str): 'r' for right - 'l' for left - 'u' for up - 'd' for down - 'c' for crossed - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.look", - {'side': side})) - - def eyes_color(self, r=255, g=255, b=255): - """Change the eye color to the given RGB color - Args: - r (int): 0-255, red value - g (int): 0-255, green value - b (int): 0-255, blue value - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.color", - {'r': r, 'g': g, 'b': b})) - - def eyes_setpixel(self, idx, r=255, g=255, b=255): - """Set individual pixels of the Mark 1 neopixel eyes - Args: - idx (int): 0-11 for the right eye, 12-23 for the left - r (int): The red value to apply - g (int): The green value to apply - b (int): The blue value to apply - """ - source_message = self._get_source_message() - if idx < 0 or idx > 23: - raise ValueError(f'idx ({idx}) must be between 0-23') - self.bus.emit(source_message.forward("enclosure.eyes.setpixel", - {'idx': idx, - 'r': r, 'g': g, 'b': b})) - - def eyes_fill(self, percentage): - """Use the eyes as a type of progress meter - Args: - percentage (int): 0-49 fills the right eye, 50-100 also covers left - """ - source_message = self._get_source_message() - if percentage < 0 or percentage > 100: - raise ValueError(f'percentage ({percentage}) must be between 0-100') - self.bus.emit(source_message.forward("enclosure.eyes.fill", - {'percentage': percentage})) - - def eyes_brightness(self, level=30): - """Set the brightness of the eyes in the display. - Args: - level (int): 1-30, bigger numbers being brighter +try: + from ovos_bus_client.apis.enclosure import EnclosureApi + +except ImportError: + from ovos_utils.messagebus import FakeMessage as Message, dig_for_message + + class EnclosureAPI: """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.level", - {'level': level})) - - def eyes_reset(self): - """Restore the eyes to their default (ready) state.""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.reset")) - - def eyes_spin(self): - """Make the eyes 'roll' - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.spin")) - - def eyes_timed_spin(self, length): - """Make the eyes 'roll' for the given time. - Args: - length (int): duration in milliseconds of roll, None = forever + This API is intended to be used to interface with the hardware + that is running Mycroft. It exposes all possible commands which + can be sent to a Mycroft enclosure implementation. + + Different enclosure implementations may implement this differently + and/or may ignore certain API calls completely. For example, + the eyes_color() API might be ignore on a Mycroft that uses simple + LEDs which only turn on/off, or not at all on an implementation + where there is no face at all. """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.eyes.timedspin", - {'length': length})) - - def eyes_volume(self, volume): - """Indicate the volume using the eyes - Args: - volume (int): 0 to 11 - """ - source_message = self._get_source_message() - if volume < 0 or volume > 11: - raise ValueError('volume ({}) must be between 0-11'. - format(str(volume))) - self.bus.emit(source_message.forward("enclosure.eyes.volume", - {'volume': volume})) - - def mouth_reset(self): - """Restore the mouth display to normal (blank)""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.reset")) - - def mouth_talk(self): - """Show a generic 'talking' animation for non-synched speech""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.talk")) - - def mouth_think(self): - """Show a 'thinking' image or animation""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.think")) - - def mouth_listen(self): - """Show a 'thinking' image or animation""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.listen")) - - def mouth_smile(self): - """Show a 'smile' image or animation""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.smile")) - - def mouth_viseme(self, start, viseme_pairs): - """ Send mouth visemes as a list in a single message. - + + def __init__(self, bus=None, skill_id=""): + self.bus = bus + self.skill_id = skill_id + + def set_bus(self, bus): + self.bus = bus + + def set_id(self, skill_id): + self.skill_id = skill_id + + def _get_source_message(self): + return dig_for_message() or \ + Message("", context={"destination": ["enclosure"], + "skill_id": self.skill_id}) + + def register(self, skill_id=""): + """Registers a skill as active. Used for speak() and speak_dialog() + to 'patch' a previous implementation. Somewhat hacky. + DEPRECATED - unused + """ + source_message = self._get_source_message() + skill_id = skill_id or self.skill_id + self.bus.emit(source_message.forward("enclosure.active_skill", + {"skill_id": skill_id})) + + def reset(self): + """The enclosure should restore itself to a started state. + Typically this would be represented by the eyes being 'open' + and the mouth reset to its default (smile or blank). + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.reset")) + + def system_reset(self): + """The enclosure hardware should reset any CPUs, etc.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.reset")) + + def system_mute(self): + """Mute (turn off) the system speaker.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.mute")) + + def system_unmute(self): + """Unmute (turn on) the system speaker.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.unmute")) + + def system_blink(self, times): + """The 'eyes' should blink the given number of times. Args: - start (int): Timestamp for start of speech - viseme_pairs: Pairs of viseme id and cumulative end times - (code, end time) - - codes: - 0 = shape for sounds like 'y' or 'aa' - 1 = shape for sounds like 'aw' - 2 = shape for sounds like 'uh' or 'r' - 3 = shape for sounds like 'th' or 'sh' - 4 = neutral shape for no sound - 5 = shape for sounds like 'f' or 'v' - 6 = shape for sounds like 'oy' or 'ao' - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.viseme_list", - {"start": start, - "visemes": viseme_pairs})) - - def mouth_text(self, text=""): - """Display text (scrolling as needed) - Args: - text (str): text string to display - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.text", - {'text': text})) - - def mouth_display(self, img_code="", x=0, y=0, refresh=True): - """Display images on faceplate. Currently supports images up to 16x8, - or half the face. You can use the 'x' parameter to cover the other - half of the faceplate. - Args: - img_code (str): text string that encodes a black and white image - x (int): x offset for image - y (int): y offset for image - refresh (bool): specify whether to clear the faceplate before - displaying the new image or not. - Useful if you'd like to display multiple images - on the faceplate at once. - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward('enclosure.mouth.display', - {'img_code': img_code, - 'xOffset': x, - 'yOffset': y, - 'clearPrev': refresh})) - - def mouth_display_png(self, image_absolute_path, - invert=False, x=0, y=0, refresh=True): - """ Send an image to the enclosure. - - Args: - image_absolute_path (string): The absolute path of the image - invert (bool): inverts the image being drawn. - x (int): x offset for image - y (int): y offset for image - refresh (bool): specify whether to clear the faceplate before - displaying the new image or not. - Useful if you'd like to display muliple images - on the faceplate at once. - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.mouth.display_image", - {'img_path': image_absolute_path, - 'xOffset': x, - 'yOffset': y, - 'invert': invert, - 'clearPrev': refresh})) - - def weather_display(self, img_code, temp): - """Show a the temperature and a weather icon - - Args: - img_code (char): one of the following icon codes - 0 = sunny - 1 = partly cloudy - 2 = cloudy - 3 = light rain - 4 = raining - 5 = stormy - 6 = snowing - 7 = wind/mist - temp (int): the temperature (either C or F, not indicated) - """ - source_message = self._get_source_message() - self.bus.emit(source_message.forward("enclosure.weather.display", - {'img_code': img_code, - 'temp': temp})) - - def activate_mouth_events(self): - """Enable movement of the mouth with speech""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward('enclosure.mouth.events.activate')) - - def deactivate_mouth_events(self): - """Disable movement of the mouth with speech""" - source_message = self._get_source_message() - self.bus.emit(source_message.forward( - 'enclosure.mouth.events.deactivate')) - - def get_eyes_color(self): - """Get the eye RGB color for all pixels - Returns: - (list) pixels - list of (r,g,b) tuples for each eye pixel - """ - source_message = self._get_source_message() - message = source_message.forward("enclosure.eyes.rgb.get") - response = self.bus.wait_for_response(message, "enclosure.eyes.rgb") - if response: - return response.data["pixels"] - raise TimeoutError("Enclosure took too long to respond") - - def get_eyes_pixel_color(self, idx): - """Get the RGB color for a specific eye pixel - Returns: - (r,g,b) tuples for selected pixel - """ - if idx < 0 or idx > 23: - raise ValueError(f'idx ({idx}) must be between 0-23') - return self.get_eyes_color()[idx] + times (int): number of times to blink + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.system.blink", + {'times': times})) + + def eyes_on(self): + """Illuminate or show the eyes.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.on")) + + def eyes_off(self): + """Turn off or hide the eyes.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.off")) + + def eyes_blink(self, side): + """Make the eyes blink + Args: + side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.blink", + {'side': side})) + + def eyes_narrow(self): + """Make the eyes look narrow, like a squint""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.narrow")) + + def eyes_look(self, side): + """Make the eyes look to the given side + Args: + side (str): 'r' for right + 'l' for left + 'u' for up + 'd' for down + 'c' for crossed + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.look", + {'side': side})) + + def eyes_color(self, r=255, g=255, b=255): + """Change the eye color to the given RGB color + Args: + r (int): 0-255, red value + g (int): 0-255, green value + b (int): 0-255, blue value + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.color", + {'r': r, 'g': g, 'b': b})) + + def eyes_setpixel(self, idx, r=255, g=255, b=255): + """Set individual pixels of the Mark 1 neopixel eyes + Args: + idx (int): 0-11 for the right eye, 12-23 for the left + r (int): The red value to apply + g (int): The green value to apply + b (int): The blue value to apply + """ + source_message = self._get_source_message() + if idx < 0 or idx > 23: + raise ValueError(f'idx ({idx}) must be between 0-23') + self.bus.emit(source_message.forward("enclosure.eyes.setpixel", + {'idx': idx, + 'r': r, 'g': g, 'b': b})) + + def eyes_fill(self, percentage): + """Use the eyes as a type of progress meter + Args: + percentage (int): 0-49 fills the right eye, 50-100 also covers left + """ + source_message = self._get_source_message() + if percentage < 0 or percentage > 100: + raise ValueError(f'percentage ({percentage}) must be between 0-100') + self.bus.emit(source_message.forward("enclosure.eyes.fill", + {'percentage': percentage})) + + def eyes_brightness(self, level=30): + """Set the brightness of the eyes in the display. + Args: + level (int): 1-30, bigger numbers being brighter + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.level", + {'level': level})) + + def eyes_reset(self): + """Restore the eyes to their default (ready) state.""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.reset")) + + def eyes_spin(self): + """Make the eyes 'roll' + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.spin")) + + def eyes_timed_spin(self, length): + """Make the eyes 'roll' for the given time. + Args: + length (int): duration in milliseconds of roll, None = forever + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.eyes.timedspin", + {'length': length})) + + def eyes_volume(self, volume): + """Indicate the volume using the eyes + Args: + volume (int): 0 to 11 + """ + source_message = self._get_source_message() + if volume < 0 or volume > 11: + raise ValueError('volume ({}) must be between 0-11'. + format(str(volume))) + self.bus.emit(source_message.forward("enclosure.eyes.volume", + {'volume': volume})) + + def mouth_reset(self): + """Restore the mouth display to normal (blank)""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.reset")) + + def mouth_talk(self): + """Show a generic 'talking' animation for non-synched speech""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.talk")) + + def mouth_think(self): + """Show a 'thinking' image or animation""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.think")) + + def mouth_listen(self): + """Show a 'thinking' image or animation""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.listen")) + + def mouth_smile(self): + """Show a 'smile' image or animation""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.smile")) + + def mouth_viseme(self, start, viseme_pairs): + """ Send mouth visemes as a list in a single message. + + Args: + start (int): Timestamp for start of speech + viseme_pairs: Pairs of viseme id and cumulative end times + (code, end time) + + codes: + 0 = shape for sounds like 'y' or 'aa' + 1 = shape for sounds like 'aw' + 2 = shape for sounds like 'uh' or 'r' + 3 = shape for sounds like 'th' or 'sh' + 4 = neutral shape for no sound + 5 = shape for sounds like 'f' or 'v' + 6 = shape for sounds like 'oy' or 'ao' + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.viseme_list", + {"start": start, + "visemes": viseme_pairs})) + + def mouth_text(self, text=""): + """Display text (scrolling as needed) + Args: + text (str): text string to display + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.text", + {'text': text})) + + def mouth_display(self, img_code="", x=0, y=0, refresh=True): + """Display images on faceplate. Currently supports images up to 16x8, + or half the face. You can use the 'x' parameter to cover the other + half of the faceplate. + Args: + img_code (str): text string that encodes a black and white image + x (int): x offset for image + y (int): y offset for image + refresh (bool): specify whether to clear the faceplate before + displaying the new image or not. + Useful if you'd like to display multiple images + on the faceplate at once. + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward('enclosure.mouth.display', + {'img_code': img_code, + 'xOffset': x, + 'yOffset': y, + 'clearPrev': refresh})) + + def mouth_display_png(self, image_absolute_path, + invert=False, x=0, y=0, refresh=True): + """ Send an image to the enclosure. + + Args: + image_absolute_path (string): The absolute path of the image + invert (bool): inverts the image being drawn. + x (int): x offset for image + y (int): y offset for image + refresh (bool): specify whether to clear the faceplate before + displaying the new image or not. + Useful if you'd like to display muliple images + on the faceplate at once. + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.mouth.display_image", + {'img_path': image_absolute_path, + 'xOffset': x, + 'yOffset': y, + 'invert': invert, + 'clearPrev': refresh})) + + def weather_display(self, img_code, temp): + """Show a the temperature and a weather icon + + Args: + img_code (char): one of the following icon codes + 0 = sunny + 1 = partly cloudy + 2 = cloudy + 3 = light rain + 4 = raining + 5 = stormy + 6 = snowing + 7 = wind/mist + temp (int): the temperature (either C or F, not indicated) + """ + source_message = self._get_source_message() + self.bus.emit(source_message.forward("enclosure.weather.display", + {'img_code': img_code, + 'temp': temp})) + + def activate_mouth_events(self): + """Enable movement of the mouth with speech""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward('enclosure.mouth.events.activate')) + + def deactivate_mouth_events(self): + """Disable movement of the mouth with speech""" + source_message = self._get_source_message() + self.bus.emit(source_message.forward( + 'enclosure.mouth.events.deactivate')) + + def get_eyes_color(self): + """Get the eye RGB color for all pixels + Returns: + (list) pixels - list of (r,g,b) tuples for each eye pixel + """ + source_message = self._get_source_message() + message = source_message.forward("enclosure.eyes.rgb.get") + response = self.bus.wait_for_response(message, "enclosure.eyes.rgb") + if response: + return response.data["pixels"] + raise TimeoutError("Enclosure took too long to respond") + + def get_eyes_pixel_color(self, idx): + """Get the RGB color for a specific eye pixel + Returns: + (r,g,b) tuples for selected pixel + """ + if idx < 0 or idx > 23: + raise ValueError(f'idx ({idx}) must be between 0-23') + return self.get_eyes_color()[idx] diff --git a/ovos_utils/enclosure/mark1/__init__.py b/ovos_utils/enclosure/mark1/__init__.py index 62b3992d..5e4766ac 100644 --- a/ovos_utils/enclosure/mark1/__init__.py +++ b/ovos_utils/enclosure/mark1/__init__.py @@ -1,4 +1,8 @@ from ovos_utils.enclosure.api import EnclosureAPI +from ovos_utils.log import LOG + +LOG.warning("ovos_utils.enclosure.mark1 moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" + " this module will be removed in version 0.1.0") class Mark1EnclosureAPI(EnclosureAPI): diff --git a/ovos_utils/enclosure/mark1/eyes/__init__.py b/ovos_utils/enclosure/mark1/eyes/__init__.py index 56fb3c73..48743c48 100644 --- a/ovos_utils/enclosure/mark1/eyes/__init__.py +++ b/ovos_utils/enclosure/mark1/eyes/__init__.py @@ -1,512 +1,513 @@ -from ovos_utils.enclosure.mark1 import Mark1EnclosureAPI -from ovos_utils.messagebus import get_mycroft_bus -from ovos_utils.colors import Color -from ovos_utils import rotate_list -from time import sleep - - -class EyePixel: - def __init__(self, index, api, color=None): - self.index = index - self.api = api - self.color = color or Color() - - @property - def rgb(self): - return self.color.rgb255 - - def sync_color(self): - r, g, b = self.api.get_eyes_pixel_color(self.index) - color = Color.from_rgb(r, g, b) - self.change_color(color) - - def update_color(self): - self.change_color(self.color) - - def change_color(self, name): - if isinstance(name, str): - self.color = Color.from_name(name) - elif isinstance(name, Color): - self.color = name - else: - raise ValueError("not a Color object") - r, g, b = self.rgb - self.api.eyes_setpixel(self.index, r, g, b) - - def set_saturation(self, value): - self.color.set_saturation(value) - self.update_color() - - def set_luminance(self, value): - self.color.set_luminance(value) - self.update_color() - - def set_hue(self, value): - self.color.set_hue(value) - self.update_color() - - def __repr__(self): - return "Pixel_" + str(self.index) + ":" + self.color.color_description - - -class Eye(list): - def __init__(self, pixel_range, bus=None, color=None): - super().__init__() - self.bus = bus or get_mycroft_bus() - self.api = Mark1EnclosureAPI(self.bus) - for idx in range(pixel_range[0], pixel_range[1]): - pixel = EyePixel(idx, self.api) - self.append(pixel) - self.color = Color() - if color: +from ovos_utils.log import LOG + +LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" + " this module will be removed in version 0.1.0") + +try: + from ovos_mark1.eyes import * +except: + from ovos_utils.enclosure.mark1 import Mark1EnclosureAPI + from ovos_utils.messagebus import get_mycroft_bus + from ovos_utils.colors import Color + from ovos_utils import rotate_list + from time import sleep + + + class EyePixel: + def __init__(self, index, api, color=None): + self.index = index + self.api = api + self.color = color or Color() + + @property + def rgb(self): + return self.color.rgb255 + + def sync_color(self): + r, g, b = self.api.get_eyes_pixel_color(self.index) + color = Color.from_rgb(r, g, b) self.change_color(color) - else: - self.sync_color() - def sync_color(self): - for p in self: - p.sync_color() - sleep(0.05) - - def update_color(self): - for p in self: - p.update_color() - sleep(0.05) + def update_color(self): + self.change_color(self.color) + + def change_color(self, name): + if isinstance(name, str): + self.color = Color.from_name(name) + elif isinstance(name, Color): + self.color = name + else: + raise ValueError("not a Color object") + r, g, b = self.rgb + self.api.eyes_setpixel(self.index, r, g, b) + + def set_saturation(self, value): + self.color.set_saturation(value) + self.update_color() + + def set_luminance(self, value): + self.color.set_luminance(value) + self.update_color() + + def set_hue(self, value): + self.color.set_hue(value) + self.update_color() + + def __repr__(self): + return "Pixel_" + str(self.index) + ":" + self.color.color_description + + + class Eye(list): + def __init__(self, pixel_range, bus=None, color=None): + super().__init__() + self.bus = bus or get_mycroft_bus() + self.api = Mark1EnclosureAPI(self.bus) + for idx in range(pixel_range[0], pixel_range[1]): + pixel = EyePixel(idx, self.api) + self.append(pixel) + self.color = Color() + if color: + self.change_color(color) + else: + self.sync_color() + + def sync_color(self): + for p in self: + p.sync_color() + sleep(0.05) + + def update_color(self): + for p in self: + p.update_color() + sleep(0.05) + + def change_color(self, name): + if isinstance(name, str): + self.color = Color.from_name(name) + elif isinstance(name, Color): + self.color = name + else: + raise ValueError("not a Color object") + for led in self: + led.change_color(self.color) + # writer bugs out if messages sent too fast + sleep(0.05) + + def saturation_spin(self, speed=0.05): + values = [] + for idx, pixel in enumerate(self): + sat = 0.09 * idx + pixel.set_saturation(sat) + values.append(sat) + sleep(speed) - def change_color(self, name): - if isinstance(name, str): - self.color = Color.from_name(name) - elif isinstance(name, Color): - self.color = name - else: - raise ValueError("not a Color object") - for led in self: - led.change_color(self.color) - # writer bugs out if messages sent too fast - sleep(0.05) + while True: + values = rotate_list(values, -1) + for idx, value in enumerate(values): + self[idx].set_saturation(value) + sleep(speed) - def saturation_spin(self, speed=0.05): - values = [] - for idx, pixel in enumerate(self): - sat = 0.09 * idx - pixel.set_saturation(sat) - values.append(sat) - sleep(speed) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self[idx].set_saturation(value) + def luminance_spin(self, speed=0.05): + values = [] + for idx, pixel in enumerate(self): + sat = 0.05 * idx + pixel.set_luminance(sat) + values.append(sat) sleep(speed) - def luminance_spin(self, speed=0.05): - values = [] - for idx, pixel in enumerate(self): - sat = 0.05 * idx - pixel.set_luminance(sat) - values.append(sat) - sleep(speed) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self[idx].set_luminance(value) + while True: + values = rotate_list(values, -1) + for idx, value in enumerate(values): + self[idx].set_luminance(value) + sleep(speed) + + def hue_spin(self, speed=0.05): + values = [] + for idx, pixel in enumerate(self): + sat = 0.083 * idx + pixel.set_hue(sat) + values.append(sat) sleep(speed) - def hue_spin(self, speed=0.05): - values = [] - for idx, pixel in enumerate(self): - sat = 0.083 * idx - pixel.set_hue(sat) - values.append(sat) - sleep(speed) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self[idx].set_hue(value) + while True: + values = rotate_list(values, -1) + for idx, value in enumerate(values): + self[idx].set_hue(value) + sleep(speed) + + def set_hue(self, hue): + for pixel in self: + pixel.color.set_hue(hue) + pixel.update_color() + + def set_luminance(self, value): + for pixel in self: + pixel.color.set_luminance(value) + self.update_color() + + def set_saturation(self, value): + for pixel in self: + pixel.color.set_saturation(value) + self.update_color() + + def on(self): + self.set_luminance(1) + + def off(self): + self.set_luminance(0) + + def blink_once(self): + """ + Make the eye blink + """ + raise NotImplementedError + + def blink(self, speed=0.5): + """ + Make the right eye blink in a loop + """ + while True: + self.blink_once() sleep(speed) - def set_hue(self, hue): - for pixel in self: - pixel.color.set_hue(hue) - pixel.update_color() - - def set_luminance(self, value): - for pixel in self: - pixel.color.set_luminance(value) - self.update_color() - - def set_saturation(self, value): - for pixel in self: - pixel.color.set_saturation(value) - self.update_color() - - def on(self): - self.set_luminance(1) - - def off(self): - self.set_luminance(0) - - def blink_once(self): - """ - Make the eye blink - """ - raise NotImplementedError - - def blink(self, speed=0.5): - """ - Make the right eye blink in a loop - """ - while True: - self.blink_once() - sleep(speed) - - -class RightEye(Eye): - def __init__(self, bus, color=None): - super().__init__(bus=bus, pixel_range=(12, 24), color=color) - - def sync_color(self): - pixels = self.api.get_eyes_color()[12:] - for idx, (r, g, b) in enumerate(pixels): - self[idx].color = Color.from_rgb(r, g, b) - self.update_color() - - def blink_once(self): - """ - Make the right eye blink - """ - self.api.eyes_blink("r") - - -class LeftEye(Eye): - def __init__(self, bus, color=None): - super().__init__(bus=bus, pixel_range=(0, 12), color=color) - - def sync_color(self): - pixels = self.api.get_eyes_color()[:12] - for idx, (r, g, b) in enumerate(pixels): - self[idx].color = Color.from_rgb(r, g, b) - self.update_color() - - def blink_once(self): - """ - Make the left eye blink - """ - self.api.eyes_blink("l") - - -class Eyes(list): - def __init__(self, bus=None, color=None): - super().__init__() - self.bus = bus or get_mycroft_bus() - self.api = Mark1EnclosureAPI(self.bus) - self.right = RightEye(self.bus) - self.left = LeftEye(self.bus) - self.color = Color() - if color: - self.change_color(color) - else: - self.sync_color() - - def __getitem__(self, item): - assert isinstance(item, int) - assert 0 <= item <= 23 - if item < 12: - return self.left[item] - return self.right[item - 12] - - def __setitem__(self, key, value): - assert isinstance(key, int) - assert 0 <= key <= 23 - if key < 12: - self.left[key] = value - self.right[key] = value - - def __iter__(self): - for i in range(len(self)): - yield self[i] - - def __len__(self): - return len(self.left) + len(self.right) - - def sync_color(self): - """ updates internal color value to current color """ - pixels = self.api.get_eyes_color() - for idx, (r, g, b) in enumerate(pixels): - self[idx].color = Color.from_rgb(r, g, b) - - def update_color(self): - """ updates arduino color to current pixels """ - for i in range(len(self) // 2): - self.left[i].update_color() - sleep(0.05) - self.right[i].update_color() - sleep(0.05) - def change_color(self, name): - """ changes color of both eyes """ - if isinstance(name, str): - self.color = Color.from_name(name) - elif isinstance(name, Color): - self.color = name - else: - raise ValueError("not a Color object") - r, g, b = self.color.rgb255 - self.api.eyes_color(r, g, b) - for idx in range(len(self)): - self[idx].color = self.color - - # animations - def saturation_spin(self, speed=0.05): - values = [] - for idx in range(len(self) // 2): - sat = 0.09 * idx - values.append(sat) - self.left[idx].set_saturation(sat) - sleep(0.03) - self.right[idx].set_saturation(sat) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self.left[idx].set_saturation(value) + class RightEye(Eye): + def __init__(self, bus, color=None): + super().__init__(bus=bus, pixel_range=(12, 24), color=color) + + def sync_color(self): + pixels = self.api.get_eyes_color()[12:] + for idx, (r, g, b) in enumerate(pixels): + self[idx].color = Color.from_rgb(r, g, b) + self.update_color() + + def blink_once(self): + """ + Make the right eye blink + """ + self.api.eyes_blink("r") + + + class LeftEye(Eye): + def __init__(self, bus, color=None): + super().__init__(bus=bus, pixel_range=(0, 12), color=color) + + def sync_color(self): + pixels = self.api.get_eyes_color()[:12] + for idx, (r, g, b) in enumerate(pixels): + self[idx].color = Color.from_rgb(r, g, b) + self.update_color() + + def blink_once(self): + """ + Make the left eye blink + """ + self.api.eyes_blink("l") + + + class Eyes(list): + def __init__(self, bus=None, color=None): + super().__init__() + self.bus = bus or get_mycroft_bus() + self.api = Mark1EnclosureAPI(self.bus) + self.right = RightEye(self.bus) + self.left = LeftEye(self.bus) + self.color = Color() + if color: + self.change_color(color) + else: + self.sync_color() + + def __getitem__(self, item): + assert isinstance(item, int) + assert 0 <= item <= 23 + if item < 12: + return self.left[item] + return self.right[item - 12] + + def __setitem__(self, key, value): + assert isinstance(key, int) + assert 0 <= key <= 23 + if key < 12: + self.left[key] = value + self.right[key] = value + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __len__(self): + return len(self.left) + len(self.right) + + def sync_color(self): + """ updates internal color value to current color """ + pixels = self.api.get_eyes_color() + for idx, (r, g, b) in enumerate(pixels): + self[idx].color = Color.from_rgb(r, g, b) + + def update_color(self): + """ updates arduino color to current pixels """ + for i in range(len(self) // 2): + self.left[i].update_color() + sleep(0.05) + self.right[i].update_color() + sleep(0.05) + + def change_color(self, name): + """ changes color of both eyes """ + if isinstance(name, str): + self.color = Color.from_name(name) + elif isinstance(name, Color): + self.color = name + else: + raise ValueError("not a Color object") + r, g, b = self.color.rgb255 + self.api.eyes_color(r, g, b) + for idx in range(len(self)): + self[idx].color = self.color + + # animations + def saturation_spin(self, speed=0.05): + values = [] + for idx in range(len(self) // 2): + sat = 0.09 * idx + values.append(sat) + self.left[idx].set_saturation(sat) sleep(0.03) - self.right[idx].set_saturation(value) - sleep(speed) + self.right[idx].set_saturation(sat) + + while True: + values = rotate_list(values, -1) + for idx, value in enumerate(values): + self.left[idx].set_saturation(value) + sleep(0.03) + self.right[idx].set_saturation(value) + sleep(speed) - def luminance_spin(self, speed=0.05): - values = [] - for idx in range(len(self) // 2): - sat = 0.05 * idx - values.append(sat) - self.left[idx].set_luminance(sat) - sleep(0.03) - self.right[idx].set_luminance(sat) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - self.left[idx].set_luminance(value) + def luminance_spin(self, speed=0.05): + values = [] + for idx in range(len(self) // 2): + sat = 0.05 * idx + values.append(sat) + self.left[idx].set_luminance(sat) sleep(0.03) - self.right[idx].set_luminance(value) - sleep(speed) + self.right[idx].set_luminance(sat) + + while True: + values = rotate_list(values, -1) + for idx, value in enumerate(values): + self.left[idx].set_luminance(value) + sleep(0.03) + self.right[idx].set_luminance(value) + sleep(speed) - def hue_spin(self, speed=0.05): - values = [] - for idx in range(len(self) // 2): - sat = 0.083 * idx - values.append(sat) - self.left[idx].set_hue(sat) - sleep(0.03) - self.right[idx].set_hue(sat) - - while True: - values = rotate_list(values, -1) - for idx, value in enumerate(values): - for pixel in self: - print(pixel) - self.left[idx].set_hue(value) + def hue_spin(self, speed=0.05): + values = [] + for idx in range(len(self) // 2): + sat = 0.083 * idx + values.append(sat) + self.left[idx].set_hue(sat) sleep(0.03) - self.right[idx].set_hue(value) - sleep(speed) + self.right[idx].set_hue(sat) + + while True: + values = rotate_list(values, -1) + for idx, value in enumerate(values): + for pixel in self: + print(pixel) + self.left[idx].set_hue(value) + sleep(0.03) + self.right[idx].set_hue(value) + sleep(speed) - def flash(self, speed=0.2): - while True: - sleep(speed) - self.on() - sleep(speed) - self.off() - - def rainbow_flash(self, speed=0.2): - colors = ["red", "orange", "yellow", "green", "cyan", "blue", - "violet", "purple"] - while True: - for color in colors: - sleep(speed) - self.off() - self.change_color(color) + def flash(self, speed=0.2): + while True: sleep(speed) self.on() - - def beacon(self, speed=0.1): - values = [i + i for i in range(30)] - while True: - for value in values: - self.set_brightness(value) sleep(speed) - values.reverse() - - def rainbow_beacon(self, speed=0.1): - values = [i + i for i in range(30)] - values += reversed(values) - colors = ["red", "orange", "yellow", "green", "cyan", "blue", - "violet", "purple"] - self.set_brightness(0) - while True: - for color in colors: - for value in values: + self.off() + + def rainbow_flash(self, speed=0.2): + colors = ["red", "orange", "yellow", "green", "cyan", "blue", + "violet", "purple"] + while True: + for color in colors: sleep(speed) + self.off() + self.change_color(color) + sleep(speed) + self.on() + + def beacon(self, speed=0.1): + values = [i + i for i in range(30)] + while True: + for value in values: self.set_brightness(value) - self.change_color(color) + sleep(speed) + values.reverse() + + def rainbow_beacon(self, speed=0.1): + values = [i + i for i in range(30)] + values += reversed(values) + colors = ["red", "orange", "yellow", "green", "cyan", "blue", + "violet", "purple"] + self.set_brightness(0) + while True: + for color in colors: + for value in values: + sleep(speed) + self.set_brightness(value) + self.change_color(color) + + # Arduino API + def set_hue(self, hue): + self.right.set_hue(hue) + sleep(0.05) + self.left.set_hue(hue) + + def set_brightness(self, level): + """ + Set the brightness of the eyes in the display. + Args: + level (int): 1-30, bigger numbers being brighter + """ + self.api.eyes_brightness(level) + + def spin(self): + self.api.eyes_spin() + + def timed_spin(self, length): + self.api.eyes_timed_spin(length) + + def reset(self): + self.api.eyes_reset() + + def fill_once(self, percent): + """ + Use the eyes as a type of progress meter + Args: + percent (int): 0-49 fills the right eye, 50-100 also covers left + """ + self.api.eyes_fill(percent) + + def look(self, side): + """Make the eyes look to the given side + Args: + side (str): 'r' for right + 'l' for left + 'u' for up + 'd' for down + 'c' for crossed + """ + self.api.eyes_look(side) + + def look_right(self): + self.look("r") + + def look_left(self): + self.look("l") + + def look_up(self): + self.look("u") + + def look_down(self): + self.look("d") + + def cross(self): + self.look("c") + + def narrow(self): + """Make the eyes look narrow, like a squint""" + self.api.eyes_narrow() + + def on(self): + """Illuminate or show the eyes.""" + self.api.eyes_on() + + def off(self): + """Turn off or hide the eyes.""" + self.api.eyes_off() + + def blink_once(self, side="b"): + """Make the eyes blink + Args: + side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' + """ + self.api.eyes_blink(side) + + def blink_right_once(self): + self.right.blink_once() + + def blink_left_once(self): + self.left.blink_once() + + def blink(self, speed=0.5): + """ + Make the eyes blink in a loop + """ + while True: + self.blink_once() + sleep(speed) + + def blink_right(self, speed=0.5): + """ + Make the right eye blink in a loop + """ + self.right.blink(speed) + + def blink_left(self, speed=0.5): + """ + Make the left eyes blink in a loop + """ + self.left.blink(speed) + + def blink_alternate(self, speed=0.5): + """ + Make the eyes blink in a loop + """ + while True: + self.blink_right_once() + sleep(speed) + self.blink_left_once() + sleep(speed) - # Arduino API - def set_hue(self, hue): - self.right.set_hue(hue) - sleep(0.05) - self.left.set_hue(hue) - - def set_brightness(self, level): - """ - Set the brightness of the eyes in the display. - Args: - level (int): 1-30, bigger numbers being brighter - """ - self.api.eyes_brightness(level) - - def spin(self): - self.api.eyes_spin() - - def timed_spin(self, length): - self.api.eyes_timed_spin(length) - - def reset(self): - self.api.eyes_reset() - - def fill_once(self, percent): - """ - Use the eyes as a type of progress meter - Args: - percent (int): 0-49 fills the right eye, 50-100 also covers left - """ - self.api.eyes_fill(percent) - - def look(self, side): - """Make the eyes look to the given side - Args: - side (str): 'r' for right - 'l' for left - 'u' for up - 'd' for down - 'c' for crossed - """ - self.api.eyes_look(side) - - def look_right(self): - self.look("r") - - def look_left(self): - self.look("l") - - def look_up(self): - self.look("u") - - def look_down(self): - self.look("d") - - def cross(self): - self.look("c") - - def narrow(self): - """Make the eyes look narrow, like a squint""" - self.api.eyes_narrow() - - def on(self): - """Illuminate or show the eyes.""" - self.api.eyes_on() - - def off(self): - """Turn off or hide the eyes.""" - self.api.eyes_off() - - def blink_once(self, side="b"): - """Make the eyes blink - Args: - side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' - """ - self.api.eyes_blink(side) - - def blink_right_once(self): - self.right.blink_once() - - def blink_left_once(self): - self.left.blink_once() - - def blink(self, speed=0.5): - """ - Make the eyes blink in a loop - """ - while True: - self.blink_once() - sleep(speed) - - def blink_right(self, speed=0.5): - """ - Make the right eye blink in a loop - """ - self.right.blink(speed) - - def blink_left(self, speed=0.5): - """ - Make the left eyes blink in a loop - """ - self.left.blink(speed) - - def blink_alternate(self, speed=0.5): - """ - Make the eyes blink in a loop - """ - while True: - self.blink_right_once() - sleep(speed) - self.blink_left_once() - sleep(speed) - - def up_down(self, speed=0.8): - """ - Make the eyes blink in a loop - """ - while True: - self.look_up() - sleep(speed) - self.look_down() - sleep(speed) - - def left_right(self, speed=0.8): - """ - Make the eyes blink in a loop - """ - while True: - self.look_left() - sleep(speed) - self.look_right() - sleep(speed) - - def fill(self, speed=0.1): - values = [i for i in range(101)] - values += reversed(values) - while True: - for percent in values: - self.fill_once(percent) + def up_down(self, speed=0.8): + """ + Make the eyes blink in a loop + """ + while True: + self.look_up() + sleep(speed) + self.look_down() sleep(speed) - def rainbow_fill(self, speed=0.1): - values = [i for i in range(101)] - values += reversed(values) - colors = ["red", "orange", "yellow", "green", "cyan", "blue", - "violet", "purple"] - while True: - for color in colors: + def left_right(self, speed=0.8): + """ + Make the eyes blink in a loop + """ + while True: + self.look_left() + sleep(speed) + self.look_right() + sleep(speed) + + def fill(self, speed=0.1): + values = [i for i in range(101)] + values += reversed(values) + while True: for percent in values: self.fill_once(percent) sleep(speed) - if percent == 100: - self.change_color(color) - - -if __name__ == "__main__": - bus = get_mycroft_bus("192.168.1.70") - eyes = Eyes(bus) - eyes.hue_spin() + def rainbow_fill(self, speed=0.1): + values = [i for i in range(101)] + values += reversed(values) + colors = ["red", "orange", "yellow", "green", "cyan", "blue", + "violet", "purple"] + while True: + for color in colors: + for percent in values: + self.fill_once(percent) + sleep(speed) + if percent == 100: + self.change_color(color) diff --git a/ovos_utils/enclosure/mark1/faceplate/__init__.py b/ovos_utils/enclosure/mark1/faceplate/__init__.py index 308f7d08..64ebcce4 100644 --- a/ovos_utils/enclosure/mark1/faceplate/__init__.py +++ b/ovos_utils/enclosure/mark1/faceplate/__init__.py @@ -7,398 +7,405 @@ from collections.abc import MutableSequence import copy - -class FaceplateGrid(MutableSequence): - encoded = None - str_grid = None - pad_char = "." - - def __init__(self, grid=None, bus=None): - self.bus = bus or get_mycroft_bus() - self._api = Mark1EnclosureAPI(self.bus) - self.grid = [] - for x in range(8): - self.grid.append([]) - for y in range(32): - self.grid[x].append(0) - if self.encoded: - self.grid = self.decode(self.encoded).grid - elif self.str_grid is not None: - self.grid = FaceplateGrid(bus=self.bus)\ - .from_string(self.str_grid).grid - elif grid is not None: - self.grid = grid - - @property - def height(self): - return len(self.grid) - - @property - def width(self): - return max([len(r) for r in self.grid]) - - def display(self, invert=True, clear=True, x_offset=0, y_offset=0): - self._api.mouth_display(self.encode(invert), - x_offset, y_offset, clear) - - def print(self, draw_padding=True, invert=False): - print(self.to_string(draw_padding=draw_padding, invert=invert)) - - def encode(self, invert=False): - # to understand how this function works you need to understand how the - # Mark I arduino proprietary encoding works to display to the faceplate - - # https://mycroft-ai.gitbook.io/docs/skill-development/displaying-information/mark-1-display - - # Each char value str_gridesents a width number starting with B=1 - # then increment 1 for the next. ie C=2 - width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', - 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', - 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a'] - - height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] - - encode = width_codes[self.width - 1] - encode += height_codes[self.height - 1] - - # Turn the image pixels into binary values 1's and 0's - # the Mark I face plate encoding uses binary values to - # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...] - binary_values = [] - for i in range(self.width): # pixels - for j in range(self.height): # lines - pixels = self.grid[j] - - if pixels[i] is None: # padding - pixels[i] = 0 - - if pixels[i] != 0: - if invert is False: - binary_values.append('1') - else: - binary_values.append('0') - else: - if invert is False: - binary_values.append('0') +LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" + " this module will be removed in version 0.1.0") + + +try: + from ovos_mark1.faceplate import * +except ImportError: + + class FaceplateGrid(MutableSequence): + encoded = None + str_grid = None + pad_char = "." + + def __init__(self, grid=None, bus=None): + self.bus = bus or get_mycroft_bus() + self._api = Mark1EnclosureAPI(self.bus) + self.grid = [] + for x in range(8): + self.grid.append([]) + for y in range(32): + self.grid[x].append(0) + if self.encoded: + self.grid = self.decode(self.encoded).grid + elif self.str_grid is not None: + self.grid = FaceplateGrid(bus=self.bus)\ + .from_string(self.str_grid).grid + elif grid is not None: + self.grid = grid + + @property + def height(self): + return len(self.grid) + + @property + def width(self): + return max([len(r) for r in self.grid]) + + def display(self, invert=True, clear=True, x_offset=0, y_offset=0): + self._api.mouth_display(self.encode(invert), + x_offset, y_offset, clear) + + def print(self, draw_padding=True, invert=False): + print(self.to_string(draw_padding=draw_padding, invert=invert)) + + def encode(self, invert=False): + # to understand how this function works you need to understand how the + # Mark I arduino proprietary encoding works to display to the faceplate + + # https://mycroft-ai.gitbook.io/docs/skill-development/displaying-information/mark-1-display + + # Each char value str_gridesents a width number starting with B=1 + # then increment 1 for the next. ie C=2 + width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a'] + + height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] + + encode = width_codes[self.width - 1] + encode += height_codes[self.height - 1] + + # Turn the image pixels into binary values 1's and 0's + # the Mark I face plate encoding uses binary values to + # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...] + binary_values = [] + for i in range(self.width): # pixels + for j in range(self.height): # lines + pixels = self.grid[j] + + if pixels[i] is None: # padding + pixels[i] = 0 + + if pixels[i] != 0: + if invert is False: + binary_values.append('1') + else: + binary_values.append('0') else: - binary_values.append('1') - # these values are used to determine how binary values - # needs to be grouped together - number_of_bottom_pixel = 0 - - if self.height > 4: - number_of_top_pixel = 4 - number_of_bottom_pixel = self.height - 4 - else: - number_of_top_pixel = self.height - - # this loop will group together the individual binary values - # ie. binary_list = ['1111', '001', '0101', '100'] - binary_list = [] - binary_code = '' - increment = 0 - alternate = False - for val in binary_values: - binary_code += val - increment += 1 - if increment == number_of_top_pixel and alternate is False: + if invert is False: + binary_values.append('0') + else: + binary_values.append('1') + # these values are used to determine how binary values + # needs to be grouped together + number_of_bottom_pixel = 0 + + if self.height > 4: + number_of_top_pixel = 4 + number_of_bottom_pixel = self.height - 4 + else: + number_of_top_pixel = self.height + + # this loop will group together the individual binary values + # ie. binary_list = ['1111', '001', '0101', '100'] + binary_list = [] + binary_code = '' + increment = 0 + alternate = False + for val in binary_values: + binary_code += val + increment += 1 + if increment == number_of_top_pixel and alternate is False: + # binary code is reversed for encoding + binary_list.append(binary_code[::-1]) + increment = 0 + binary_code = '' + alternate = True + elif increment == number_of_bottom_pixel and alternate is True: + binary_list.append(binary_code[::-1]) + increment = 0 + binary_code = '' + alternate = False + # Code to let the Mark I arduino know where to place the + # pixels on the faceplate + pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'] + for binary_values in binary_list: + number = int(binary_values, 2) + pixel_code = pixel_codes[number] + encode += pixel_code + return encode + + def decode(self, encoded, invert=False, pad=True): + codes = list(encoded) + + # Each char value str_gridesents a width number starting with B=1 + # then increment 1 for the next. ie C=2 + width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a'] + + height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] + + height = height_codes.index(codes[1]) + 1 + width = width_codes.index(codes[0]) + 1 + + # Code to let the Mark I arduino know where to place the + # pixels on the faceplate + pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'] + codes.reverse() + binary_list = [] + for pixel_code in codes[:-2]: + number = pixel_codes.index(pixel_code.upper()) + bin_str = str(bin(number))[2:] + while not len(bin_str) == 4: + bin_str = "0" + bin_str + binary_list += [bin_str] + + binary_list.reverse() + + for idx, binary_code in enumerate(binary_list): # binary code is reversed for encoding - binary_list.append(binary_code[::-1]) - increment = 0 - binary_code = '' - alternate = True - elif increment == number_of_bottom_pixel and alternate is True: - binary_list.append(binary_code[::-1]) - increment = 0 - binary_code = '' - alternate = False - # Code to let the Mark I arduino know where to place the - # pixels on the faceplate - pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'] - for binary_values in binary_list: - number = int(binary_values, 2) - pixel_code = pixel_codes[number] - encode += pixel_code - return encode - - def decode(self, encoded, invert=False, pad=True): - codes = list(encoded) - - # Each char value str_gridesents a width number starting with B=1 - # then increment 1 for the next. ie C=2 - width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', - 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', - 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a'] - - height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] - - height = height_codes.index(codes[1]) + 1 - width = width_codes.index(codes[0]) + 1 - - # Code to let the Mark I arduino know where to place the - # pixels on the faceplate - pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'] - codes.reverse() - binary_list = [] - for pixel_code in codes[:-2]: - number = pixel_codes.index(pixel_code.upper()) - bin_str = str(bin(number))[2:] - while not len(bin_str) == 4: - bin_str = "0" + bin_str - binary_list += [bin_str] - - binary_list.reverse() - - for idx, binary_code in enumerate(binary_list): - # binary code is reversed for encoding - binary_list[idx] = binary_code[::-1] - - binary_code = "".join(binary_list) - - # Turn the image pixels into binary values 1's and 0's - # the Mark I face plate encoding uses binary values to - # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...] - grid = [] - # binary_code is a sequence of column by column - cols = [list(binary_code)[x:x + height] for x in - range(0, len(list(binary_code)), height)] - - for x in range(height): - row = [] - for y in range(width): - bit = int(cols[y][x]) - if invert: - if bit: - bit = 0 - else: - bit = 1 - row.append(bit) - grid.append(row) - - # handle padding - if pad: - if width < self.width: - n = int((self.width - width) / 2) - if invert: - padding = [1] * n - else: - padding = [0] * n - for idx, row in enumerate(grid): - grid[idx] = padding + row + padding - if height < self.height: - pass # TODO vertical padding - self.grid = grid - return self - - def from_string(self, str_grid): - rows = [r for r in str_grid.split("\n") if len(r)] - grid = [] - for r in rows: - row = [] - for char in list(r): - if char == " ": - row.append(1) - elif char == FaceplateGrid.pad_char: - row.append(None) - else: - row.append(0) - while len(row) < self.width: - row.append(None) - grid.append(row) - self.grid = grid - return self - - def to_string(self, draw_padding=False, invert=False): - str_grid = "" - for row in self.grid: - line = "" - for col in row: - if col is None and draw_padding: - line += self.pad_char - elif col == 1: + binary_list[idx] = binary_code[::-1] + + binary_code = "".join(binary_list) + + # Turn the image pixels into binary values 1's and 0's + # the Mark I face plate encoding uses binary values to + # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...] + grid = [] + # binary_code is a sequence of column by column + cols = [list(binary_code)[x:x + height] for x in + range(0, len(list(binary_code)), height)] + + for x in range(height): + row = [] + for y in range(width): + bit = int(cols[y][x]) if invert: - line += "X" - else: - line += " " - elif col == 0: + if bit: + bit = 0 + else: + bit = 1 + row.append(bit) + grid.append(row) + + # handle padding + if pad: + if width < self.width: + n = int((self.width - width) / 2) if invert: - line += " " + padding = [1] * n + else: + padding = [0] * n + for idx, row in enumerate(grid): + grid[idx] = padding + row + padding + if height < self.height: + pass # TODO vertical padding + self.grid = grid + return self + + def from_string(self, str_grid): + rows = [r for r in str_grid.split("\n") if len(r)] + grid = [] + for r in rows: + row = [] + for char in list(r): + if char == " ": + row.append(1) + elif char == FaceplateGrid.pad_char: + row.append(None) else: - line += "X" - str_grid += line + "\n" - return str_grid - - def invert(self): - for x in range(self.height): - for y in range(self.width): - if self.grid[x][y] == 0: - self.grid[x][y] = 1 - elif self.grid[x][y] == 1: + row.append(0) + while len(row) < self.width: + row.append(None) + grid.append(row) + self.grid = grid + return self + + def to_string(self, draw_padding=False, invert=False): + str_grid = "" + for row in self.grid: + line = "" + for col in row: + if col is None and draw_padding: + line += self.pad_char + elif col == 1: + if invert: + line += "X" + else: + line += " " + elif col == 0: + if invert: + line += " " + else: + line += "X" + str_grid += line + "\n" + return str_grid + + def invert(self): + for x in range(self.height): + for y in range(self.width): + if self.grid[x][y] == 0: + self.grid[x][y] = 1 + elif self.grid[x][y] == 1: + self.grid[x][y] = 0 + return self + + def clear(self): + for x in range(self.height): + for y in range(self.width): self.grid[x][y] = 0 - return self - - def clear(self): - for x in range(self.height): - for y in range(self.width): - self.grid[x][y] = 0 - return self - - @property - def is_empty(self): - for x in range(self.height): - for y in range(self.width): - if self.grid[x][y] == 1: - return False - return True - - def randomize(self, n=200): - for i in range(n): - x = random.randint(0, self.height-1) - y = random.randint(0, self.width-1) - self.grid[x][y] = int(random.randint(0, 1)) - return self - - def __len__(self): - # number of pixels - return self.width * self.height + return self - def __delitem__(self, index): - self.grid.__delitem__(index) + @property + def is_empty(self): + for x in range(self.height): + for y in range(self.width): + if self.grid[x][y] == 1: + return False + return True - def insert(self, index, value): - self.grid.insert(index - 1, value) + def randomize(self, n=200): + for i in range(n): + x = random.randint(0, self.height-1) + y = random.randint(0, self.width-1) + self.grid[x][y] = int(random.randint(0, 1)) + return self - def __setitem__(self, index, value): - self.grid.__setitem__(index, value) + def __len__(self): + # number of pixels + return self.width * self.height - def __getitem__(self, index): - return self.grid.__getitem__(index) + def __delitem__(self, index): + self.grid.__delitem__(index) + def insert(self, index, value): + self.grid.insert(index - 1, value) -class FacePlateAnimation(FaceplateGrid): + def __setitem__(self, index, value): + self.grid.__setitem__(index, value) - def __init__(self, grid=None, bus=None): - super().__init__(grid, bus) - self.finished = False + def __getitem__(self, index): + return self.grid.__getitem__(index) - def animate(self): - pass - def __iter__(self): - while not self.finished: - self.animate() - yield self + class FacePlateAnimation(FaceplateGrid): - def start(self): - self.finished = False + def __init__(self, grid=None, bus=None): + super().__init__(grid, bus) + self.finished = False - def stop(self): - self.finished = True + def animate(self): + pass - def run(self, delay=0.5, callback=None, daemonic=False): - self.start() - - if delay < 0.4: - # writer bugs out if sending messages too rapidly - delay = 0.4 - - def step(callback=callback): - try: - if not self.finished: - self.animate() - if callback: - callback(self) - except Exception as e: - LOG.error(e) - - if daemonic: - create_loop(step, delay) - else: + def __iter__(self): while not self.finished: - step() - sleep(delay) - self.stop() - - def scroll_down(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - self.grid[y][x] = old[y - 1][x] - - def scroll_up(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if y == self.height - 1: - self.grid[y][x] = old[0][x] - else: - self.grid[y][x] = old[y + 1][x] - - def scroll_right(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - self.grid[y][x] = old[y][x - 1] - - def scroll_left(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if x == self.width -1: - self.grid[y][x] = old[y][0] - else: - self.grid[y][x] = old[y][x + 1] - - def move_down(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if y - 1 < 0: - self.grid[y][x] = 0 - else: + self.animate() + yield self + + def start(self): + self.finished = False + + def stop(self): + self.finished = True + + def run(self, delay=0.5, callback=None, daemonic=False): + self.start() + + if delay < 0.4: + # writer bugs out if sending messages too rapidly + delay = 0.4 + + def step(callback=callback): + try: + if not self.finished: + self.animate() + if callback: + callback(self) + except Exception as e: + LOG.error(e) + + if daemonic: + create_loop(step, delay) + else: + while not self.finished: + step() + sleep(delay) + self.stop() + + def scroll_down(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): self.grid[y][x] = old[y - 1][x] - def move_up(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if y == self.height - 1: - self.grid[y][x] = 0 - else: - self.grid[y][x] = old[y + 1][x] - - def move_right(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - self.grid[y][x] = old[y][x - 1] - - def move_left(self): - old = copy.deepcopy(self.grid) - for x in range(self.width): - for y in range(self.height): - if x == self.width - 1: - self.grid[y][x] = 0 - else: - self.grid[y][x] = old[y][x + 1] - - -class BlackScreen(FaceplateGrid): - # Basically a util class to handle - # inverting on __init__ - str_grid = """ -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.invert() + def scroll_up(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): + if y == self.height - 1: + self.grid[y][x] = old[0][x] + else: + self.grid[y][x] = old[y + 1][x] + + def scroll_right(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): + self.grid[y][x] = old[y][x - 1] + + def scroll_left(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): + if x == self.width -1: + self.grid[y][x] = old[y][0] + else: + self.grid[y][x] = old[y][x + 1] + + def move_down(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): + if y - 1 < 0: + self.grid[y][x] = 0 + else: + self.grid[y][x] = old[y - 1][x] + + def move_up(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): + if y == self.height - 1: + self.grid[y][x] = 0 + else: + self.grid[y][x] = old[y + 1][x] + + def move_right(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): + self.grid[y][x] = old[y][x - 1] + + def move_left(self): + old = copy.deepcopy(self.grid) + for x in range(self.width): + for y in range(self.height): + if x == self.width - 1: + self.grid[y][x] = 0 + else: + self.grid[y][x] = old[y][x + 1] + + + class BlackScreen(FaceplateGrid): + # Basically a util class to handle + # inverting on __init__ + str_grid = """ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.invert() diff --git a/ovos_utils/enclosure/mark1/faceplate/animations.py b/ovos_utils/enclosure/mark1/faceplate/animations.py index 942761e8..93042170 100644 --- a/ovos_utils/enclosure/mark1/faceplate/animations.py +++ b/ovos_utils/enclosure/mark1/faceplate/animations.py @@ -1,574 +1,570 @@ -from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation, BlackScreen import copy import random +from ovos_utils.log import LOG -# Base animations -# These are mostly meant to be subclassed (empty animations) -class HorizontalScroll(FacePlateAnimation): - def __init__(self, direction="right", grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("r") or direction.startswith("l") - self.direction = direction[0] - - def animate(self): - if self.direction == "r": - self.scroll_right() - else: - self.scroll_left() - if self.is_empty: - self.stop() - - -class VerticalScroll(FacePlateAnimation): - def __init__(self, direction="up", - grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("u") or direction.startswith("d") - self.direction = direction[0] - - def animate(self): - if self.direction == "u": - self.scroll_up() - else: - self.scroll_down() - if self.is_empty: - self.stop() - - -class LeftRight(FacePlateAnimation): - def __init__(self, direction="right", start="left", grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("r") or direction.startswith("l") - self.direction = direction[0] - - # start at right/left side/center - inverted = not isinstance(self, BlackScreen) - if start[0] == "l": - # left side - for y in range(self.height): - for x in range(self.width): - if not inverted and self.grid[y][x] == 1: - pass - elif inverted and self.grid[y][x] == 0: - pass - elif start[0] == "r": - pass # right side - else: - pass # center - print(self.grid[1]) - - def animate(self): - left_collision = False - right_collision = False - inverted = not isinstance(self, BlackScreen) - for y in range(self.height): - if inverted: - if self.grid[y][self.width - 1] == 0: - right_collision = True - if self.grid[y][0] == 0: - left_collision = True +LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" + " this module will be removed in version 0.1.0") + +try: + from ovos_mark1.faceplate.animations import * +except ImportError: + from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation, BlackScreen + + # Base animations + # These are mostly meant to be subclassed (empty animations) + class HorizontalScroll(FacePlateAnimation): + def __init__(self, direction="right", grid=None, bus=None): + super().__init__(grid, bus) + assert direction.startswith("r") or direction.startswith("l") + self.direction = direction[0] + + def animate(self): + if self.direction == "r": + self.scroll_right() else: - if self.grid[y][self.width - 1] == 1: - right_collision = True - if self.grid[y][0] == 1: - left_collision = True - if left_collision and right_collision: - return # No space left to animate - elif right_collision: - self.direction = "l" - elif left_collision: - self.direction = "r" - if self.direction == "r": - self.scroll_right() - else: - self.scroll_left() - if self.is_empty: - self.stop() - - -class UpDown(FacePlateAnimation): - def __init__(self, direction="up", grid=None, bus=None): - super().__init__(grid, bus) - assert direction.startswith("u") or direction.startswith("d") - self.direction = direction[0] - - def animate(self): - top_collision = False - bottom_collision = False - for x in range(self.width): - if self.grid[0][x] == 1: - top_collision = True - if self.grid[self.height - 1][x] == 1: - bottom_collision = True + self.scroll_left() + if self.is_empty: + self.stop() - if top_collision and bottom_collision: - return # No space left to animate - elif top_collision: - self.direction = "d" - elif bottom_collision: - self.direction = "u" - if self.direction == "u": - self.scroll_up() - else: - self.scroll_down() - if self.is_empty: - self.stop() - - -class CollisionBox(FacePlateAnimation): - def __init__(self, - horizontal_direction=None, - vertical_direction=None, - grid=None, bus=None): - super().__init__(grid, bus) - assert horizontal_direction is None or \ - horizontal_direction.startswith("r") or \ - horizontal_direction.startswith("l") - assert vertical_direction is None or \ - vertical_direction.startswith("u") or \ - vertical_direction.startswith("d") - self.vertical_direction = vertical_direction[0] if \ - vertical_direction else None - self.horizontal_direction = horizontal_direction[0] if \ - horizontal_direction else None - - def animate(self): - left_collision = False - right_collision = False - top_collision = False - bottom_collision = False - for y in range(self.height): - if self.grid[y][self.width - 1] == 1: - right_collision = True - if self.grid[y][0] == 1: - left_collision = True - for x in range(self.width): - if self.grid[0][x] == 1: - top_collision = True - if self.grid[self.height - 1][x] == 1: - bottom_collision = True - if top_collision and bottom_collision: - self.vertical_direction = None - elif top_collision: - self.vertical_direction = "d" - elif bottom_collision: - self.vertical_direction = "u" - - if left_collision and right_collision: - self.horizontal_direction = None - elif right_collision: - self.horizontal_direction = "l" - elif left_collision: - self.horizontal_direction = "r" - - if self.vertical_direction is None: - pass - elif self.vertical_direction == "u": - self.scroll_up() - elif self.vertical_direction == "d": - self.scroll_down() - - if self.horizontal_direction is None: - pass - elif self.horizontal_direction == "r": - self.scroll_right() - elif self.horizontal_direction == "l": - self.scroll_left() - - if self.is_empty: - self.stop() - - -# Ready to use animations -class SquareWave(HorizontalScroll): - def __init__(self, direction="r", frequency=3, - amplitude=4, grid=None, bus=None): - super().__init__(direction, grid, bus) - # frequency must be > 1 - # frequency is in number of pixels - assert 0 < frequency - # amplitude must be 2, 4 or 6. else it renders badly - # amplitude is in number of pixels - assert 0 < amplitude < self.height - assert divmod(amplitude, 2)[1] == 0 - - self.freq = frequency - self.amplitude = self.height - amplitude - - self._initial_grid() - self.invert() - - def _initial_grid(self): - # draws the initial state - count = 0 - top = True - a = self.amplitude // 2 - 1 - for x in range(self.width): - if top: - self.grid[a + 1][x] = 1 + class VerticalScroll(FacePlateAnimation): + def __init__(self, direction="up", + grid=None, bus=None): + super().__init__(grid, bus) + assert direction.startswith("u") or direction.startswith("d") + self.direction = direction[0] + + def animate(self): + if self.direction == "u": + self.scroll_up() else: - self.grid[-a - 1][x] = 1 + self.scroll_down() + if self.is_empty: + self.stop() + + + class LeftRight(FacePlateAnimation): + def __init__(self, direction="right", start="left", grid=None, bus=None): + super().__init__(grid, bus) + assert direction.startswith("r") or direction.startswith("l") + self.direction = direction[0] + + # start at right/left side/center + inverted = not isinstance(self, BlackScreen) + if start[0] == "l": + # left side + for y in range(self.height): + for x in range(self.width): + if not inverted and self.grid[y][x] == 1: + pass + elif inverted and self.grid[y][x] == 0: + pass + elif start[0] == "r": + pass # right side + else: + pass # center + print(self.grid[1]) + def animate(self): + left_collision = False + right_collision = False + inverted = not isinstance(self, BlackScreen) for y in range(self.height): - if count == 0: - self.grid[y][x] = 1 - if y <= a: - self.grid[y][x] = 0 - elif y >= self.height - a: - self.grid[y][x] = 0 - count += 1 - if count == self.freq + 1: - count = 0 - top = not top - - -class StrayDot(CollisionBox): - def __init__(self, - start_x=None, - start_y=None, - horizontal_direction=None, - vertical_direction=None, - grid=None, bus=None): - horizontal_direction = horizontal_direction or \ - random.choice(["l", "r"]) - vertical_direction = vertical_direction or \ - random.choice(["u", "d"]) - super().__init__(horizontal_direction, vertical_direction, - grid, bus) - start_x = start_x or random.randint(0, self.width - 1) - start_y = start_y or random.randint(0, self.height - 1) - self.grid[start_y][start_x] = 1 - - -class ParticleBox(FacePlateAnimation): - def __init__(self, n_particles=5, bus=None): - super().__init__(bus=bus) - assert 0 < n_particles < 11 - self.n_particles = n_particles - self.particles = [] - - class Dot: - def __init__(self, idx, x, y, vx, vy): - self.x = x - self.y = y - self.vx = vx - self.vy = vy - self.idx = idx - - for i in range(n_particles): - vx = random.choice(["l", "r"]) - vy = random.choice(["u", "d"]) - x = random.randint(0, self.width - 1) - y = random.randint(0, self.height - 1) - while self.grid[y][x] == 1: - # 2 particles can't occupy same space + if inverted: + if self.grid[y][self.width - 1] == 0: + right_collision = True + if self.grid[y][0] == 0: + left_collision = True + else: + if self.grid[y][self.width - 1] == 1: + right_collision = True + if self.grid[y][0] == 1: + left_collision = True + if left_collision and right_collision: + return # No space left to animate + elif right_collision: + self.direction = "l" + elif left_collision: + self.direction = "r" + if self.direction == "r": + self.scroll_right() + else: + self.scroll_left() + if self.is_empty: + self.stop() + + + class UpDown(FacePlateAnimation): + def __init__(self, direction="up", grid=None, bus=None): + super().__init__(grid, bus) + assert direction.startswith("u") or direction.startswith("d") + self.direction = direction[0] + + def animate(self): + top_collision = False + bottom_collision = False + for x in range(self.width): + if self.grid[0][x] == 1: + top_collision = True + if self.grid[self.height - 1][x] == 1: + bottom_collision = True + + if top_collision and bottom_collision: + return # No space left to animate + elif top_collision: + self.direction = "d" + elif bottom_collision: + self.direction = "u" + if self.direction == "u": + self.scroll_up() + else: + self.scroll_down() + if self.is_empty: + self.stop() + + + class CollisionBox(FacePlateAnimation): + def __init__(self, + horizontal_direction=None, + vertical_direction=None, + grid=None, bus=None): + super().__init__(grid, bus) + assert horizontal_direction is None or \ + horizontal_direction.startswith("r") or \ + horizontal_direction.startswith("l") + assert vertical_direction is None or \ + vertical_direction.startswith("u") or \ + vertical_direction.startswith("d") + self.vertical_direction = vertical_direction[0] if \ + vertical_direction else None + self.horizontal_direction = horizontal_direction[0] if \ + horizontal_direction else None + + def animate(self): + left_collision = False + right_collision = False + top_collision = False + bottom_collision = False + for y in range(self.height): + if self.grid[y][self.width - 1] == 1: + right_collision = True + if self.grid[y][0] == 1: + left_collision = True + for x in range(self.width): + if self.grid[0][x] == 1: + top_collision = True + if self.grid[self.height - 1][x] == 1: + bottom_collision = True + + if top_collision and bottom_collision: + self.vertical_direction = None + elif top_collision: + self.vertical_direction = "d" + elif bottom_collision: + self.vertical_direction = "u" + + if left_collision and right_collision: + self.horizontal_direction = None + elif right_collision: + self.horizontal_direction = "l" + elif left_collision: + self.horizontal_direction = "r" + + if self.vertical_direction is None: + pass + elif self.vertical_direction == "u": + self.scroll_up() + elif self.vertical_direction == "d": + self.scroll_down() + + if self.horizontal_direction is None: + pass + elif self.horizontal_direction == "r": + self.scroll_right() + elif self.horizontal_direction == "l": + self.scroll_left() + + if self.is_empty: + self.stop() + + + # Ready to use animations + class SquareWave(HorizontalScroll): + def __init__(self, direction="r", frequency=3, + amplitude=4, grid=None, bus=None): + super().__init__(direction, grid, bus) + # frequency must be > 1 + # frequency is in number of pixels + assert 0 < frequency + # amplitude must be 2, 4 or 6. else it renders badly + # amplitude is in number of pixels + assert 0 < amplitude < self.height + assert divmod(amplitude, 2)[1] == 0 + + self.freq = frequency + self.amplitude = self.height - amplitude + + self._initial_grid() + self.invert() + + def _initial_grid(self): + # draws the initial state + count = 0 + top = True + a = self.amplitude // 2 - 1 + for x in range(self.width): + if top: + self.grid[a + 1][x] = 1 + else: + self.grid[-a - 1][x] = 1 + + for y in range(self.height): + if count == 0: + self.grid[y][x] = 1 + if y <= a: + self.grid[y][x] = 0 + elif y >= self.height - a: + self.grid[y][x] = 0 + count += 1 + if count == self.freq + 1: + count = 0 + top = not top + + + class StrayDot(CollisionBox): + def __init__(self, + start_x=None, + start_y=None, + horizontal_direction=None, + vertical_direction=None, + grid=None, bus=None): + horizontal_direction = horizontal_direction or \ + random.choice(["l", "r"]) + vertical_direction = vertical_direction or \ + random.choice(["u", "d"]) + super().__init__(horizontal_direction, vertical_direction, + grid, bus) + start_x = start_x or random.randint(0, self.width - 1) + start_y = start_y or random.randint(0, self.height - 1) + self.grid[start_y][start_x] = 1 + + + class ParticleBox(FacePlateAnimation): + def __init__(self, n_particles=5, bus=None): + super().__init__(bus=bus) + assert 0 < n_particles < 11 + self.n_particles = n_particles + self.particles = [] + + class Dot: + def __init__(self, idx, x, y, vx, vy): + self.x = x + self.y = y + self.vx = vx + self.vy = vy + self.idx = idx + + for i in range(n_particles): + vx = random.choice(["l", "r"]) + vy = random.choice(["u", "d"]) x = random.randint(0, self.width - 1) y = random.randint(0, self.height - 1) - self.grid[y][x] = 1 - self.particles.append(Dot(i, x, y, vx, vy)) - - def render_particles(self): - self.clear() - for p in self.particles: - self.grid[p.y][p.x] = 1 - - def get_particle(self, x, y): - for p in self.particles: - if p.x == x and p.y == y: - return p - - def process_collisions(self): - # new particles after this turn - new_particles = copy.deepcopy(self.particles) - - # NOTE this is not a physics simulation! - # while it is behaving like an elastic collision - # if there is a 3+ particle collision results will be incorrect - # as long as only 2 particles collide it looks accurate - # max number of particles limited to 10 to minimize chance of this - # happening - for p in self.particles: - idx = p.idx - - # horizontal movement - if p.vx is None: - # not moving horizontally - if p.x != 0: - # check for collisions from left p2 -> p1 - p2 = self.get_particle(p.x - 1, p.y) - if p2 and p2.vx == "r": - # collision p2 -> p1 - if p.x == self.width - 1: - new_particles[idx].vx = None - else: - new_particles[idx].vx = "r" - new_particles[idx].x += 1 - if p.x != self.width - 1: - # check for collisions from right p1 <- p2 + while self.grid[y][x] == 1: + # 2 particles can't occupy same space + x = random.randint(0, self.width - 1) + y = random.randint(0, self.height - 1) + self.grid[y][x] = 1 + self.particles.append(Dot(i, x, y, vx, vy)) + + def render_particles(self): + self.clear() + for p in self.particles: + self.grid[p.y][p.x] = 1 + + def get_particle(self, x, y): + for p in self.particles: + if p.x == x and p.y == y: + return p + + def process_collisions(self): + # new particles after this turn + new_particles = copy.deepcopy(self.particles) + + # NOTE this is not a physics simulation! + # while it is behaving like an elastic collision + # if there is a 3+ particle collision results will be incorrect + # as long as only 2 particles collide it looks accurate + # max number of particles limited to 10 to minimize chance of this + # happening + for p in self.particles: + idx = p.idx + + # horizontal movement + if p.vx is None: + # not moving horizontally + if p.x != 0: + # check for collisions from left p2 -> p1 + p2 = self.get_particle(p.x - 1, p.y) + if p2 and p2.vx == "r": + # collision p2 -> p1 + if p.x == self.width - 1: + new_particles[idx].vx = None + else: + new_particles[idx].vx = "r" + new_particles[idx].x += 1 + if p.x != self.width - 1: + # check for collisions from right p1 <- p2 + p2 = self.get_particle(p.x + 1, p.y) + if p2 and p2.vx == "l": + # collision p1 <- p2 + if p.x == 0: + new_particles[idx].vx = None + else: + new_particles[idx].vx = "l" + new_particles[idx].x -= 1 + # moving right + elif p.vx == "r": p2 = self.get_particle(p.x + 1, p.y) - if p2 and p2.vx == "l": - # collision p1 <- p2 - if p.x == 0: + if p.x == self.width - 1: + # border collision p1 -> | + new_particles[idx].vx = "l" + new_particles[idx].x -= 1 + elif p2: + # particle collision p1 -> p2 + if p2.vx is None: + # p2 moves, p1 stops new_particles[idx].vx = None - else: - new_particles[idx].vx = "l" - new_particles[idx].x -= 1 - # moving right - elif p.vx == "r": - p2 = self.get_particle(p.x + 1, p.y) - if p.x == self.width - 1: - # border collision p1 -> | - new_particles[idx].vx = "l" - new_particles[idx].x -= 1 - elif p2: - # particle collision p1 -> p2 - if p2.vx is None: - # p2 moves, p1 stops - new_particles[idx].vx = None - elif p2.vx == "r": - # moving together + elif p2.vx == "r": + # moving together + new_particles[idx].x += 1 + elif p2 and p2.vx == "l": + if p.x == 0: + new_particles[idx].vx = None + else: + # both change direction + new_particles[idx].vx = "l" + new_particles[idx].x -= 1 + else: + # move right new_particles[idx].x += 1 - elif p2 and p2.vx == "l": - if p.x == 0: + # moving left + elif p.vx == "l": + p2 = self.get_particle(p.x - 1, p.y) + if p.x == 0: + # border collision | <- p1 + new_particles[idx].vx = "r" + new_particles[idx].x += 1 + elif p2: + # particle collision p2 <- p1 + + if p2.vx is None: + # p2 moves, p1 stops new_particles[idx].vx = None - else: - # both change direction - new_particles[idx].vx = "l" + elif p2.vx == "l": + # moving together, no collision new_particles[idx].x -= 1 - else: - # move right - new_particles[idx].x += 1 - # moving left - elif p.vx == "l": - p2 = self.get_particle(p.x - 1, p.y) - if p.x == 0: - # border collision | <- p1 - new_particles[idx].vx = "r" - new_particles[idx].x += 1 - elif p2: - # particle collision p2 <- p1 - - if p2.vx is None: - # p2 moves, p1 stops - new_particles[idx].vx = None - elif p2.vx == "l": - # moving together, no collision + elif p2.vx == "r": + if p.x == self.width - 1: + new_particles[idx].vx = None + else: + # both change direction + new_particles[idx].vx = "r" + new_particles[idx].x += 1 + else: + # move left new_particles[idx].x -= 1 - elif p2.vx == "r": - if p.x == self.width - 1: - new_particles[idx].vx = None - else: - # both change direction - new_particles[idx].vx = "r" - new_particles[idx].x += 1 - else: - # move left - new_particles[idx].x -= 1 - - # vertical movement - if p.vy is None: - # not moving vertically - if p.y != 0: - # check for collisions from top p2 -> p1 - p2 = self.get_particle(p.x, p.y - 1) - if p2 and p2.vy == "d": - if p.y == self.height - 1: + + # vertical movement + if p.vy is None: + # not moving vertically + if p.y != 0: + # check for collisions from top p2 -> p1 + p2 = self.get_particle(p.x, p.y - 1) + if p2 and p2.vy == "d": + if p.y == self.height - 1: + new_particles[idx].vy = None + else: + # collision p2 -> p1 + new_particles[idx].vy = "d" + new_particles[idx].y += 1 + if p.y != self.height - 1: + # check for collisions from bottom p1 <- p2 + p2 = self.get_particle(p.x, p.y + 1) + if p2 and p2.vy == "u": + # collision p1 <- p2 + if p.y == 0: # on top + new_particles[idx].vy = None + else: + new_particles[idx].vy = "u" + new_particles[idx].y -= 1 + # moving down + elif p.vy == "d": + p2 = self.get_particle(p.x, p.y + 1) + if p.y == self.height - 1: + # border collision p1 -> | + new_particles[idx].vy = "u" + new_particles[idx].y -= 1 + elif p2: + # particle collision p1 -> p2 + + if p2.vy is None: + # p2 moves, p1 stops new_particles[idx].vy = None - else: - # collision p2 -> p1 - new_particles[idx].vy = "d" + elif p2.vy == "d": + # moving together new_particles[idx].y += 1 - if p.y != self.height - 1: - # check for collisions from bottom p1 <- p2 - p2 = self.get_particle(p.x, p.y + 1) - if p2 and p2.vy == "u": - # collision p1 <- p2 - if p.y == 0: # on top + elif p2.vy == "u": + if p.y == 0: + new_particles[ + idx].vy = None # wall absorbed momentum + else: + # both change direction + new_particles[idx].vy = "u" + new_particles[idx].y -= 1 + else: + # move down + new_particles[idx].y += 1 + # moving up + elif p.vy == "u": + + p2 = self.get_particle(p.x, p.y - 1) + if p.y == 0: + # border collision | <- p1 + new_particles[idx].vy = "d" + new_particles[idx].y += 1 + elif p2: + # particle collision p2 <- p1 + if p2.vy is None: + # p2 moves, p1 stops new_particles[idx].vy = None - else: - new_particles[idx].vy = "u" + elif p2.vy == "u": + # moving together, no collision new_particles[idx].y -= 1 - # moving down - elif p.vy == "d": - p2 = self.get_particle(p.x, p.y + 1) - if p.y == self.height - 1: - # border collision p1 -> | - new_particles[idx].vy = "u" - new_particles[idx].y -= 1 - elif p2: - # particle collision p1 -> p2 - - if p2.vy is None: - # p2 moves, p1 stops - new_particles[idx].vy = None - elif p2.vy == "d": - # moving together - new_particles[idx].y += 1 - elif p2.vy == "u": - if p.y == 0: - new_particles[ - idx].vy = None # wall absorbed momentum - else: + elif p2.vy == "d": # both change direction - new_particles[idx].vy = "u" - new_particles[idx].y -= 1 - else: - # move down - new_particles[idx].y += 1 - # moving up - elif p.vy == "u": - - p2 = self.get_particle(p.x, p.y - 1) - if p.y == 0: - # border collision | <- p1 - new_particles[idx].vy = "d" - new_particles[idx].y += 1 - elif p2: - # particle collision p2 <- p1 - if p2.vy is None: - # p2 moves, p1 stops - new_particles[idx].vy = None - elif p2.vy == "u": - # moving together, no collision + if p.y == self.height - 1: + new_particles[idx].vy = None + else: + new_particles[idx].vy = "d" + new_particles[idx].y += 1 + else: + # move left new_particles[idx].y -= 1 - elif p2.vy == "d": - # both change direction - if p.y == self.height - 1: - new_particles[idx].vy = None - else: - new_particles[idx].vy = "d" - new_particles[idx].y += 1 - else: - # move left - new_particles[idx].y -= 1 - - # update processed particles - self.particles = new_particles - def animate(self): - self.process_collisions() - self.render_particles() + # update processed particles + self.particles = new_particles + def animate(self): + self.process_collisions() + self.render_particles() -class FallingDots(FacePlateAnimation): - def __init__(self, n=10, bus=None): - super().__init__(bus=bus) - self._create = True - assert 0 < n < 32 - self.n = n - @property - def n_dots(self): - n = 0 - for y in range(self.height): - for x in range(self.width): - if self.grid[y][x]: - n += 1 - return n - - def animate(self): - self.move_down() - if self._create: - if random.choice([True, False]): - self._create = False - x = random.randint(0, self.width - 1) - self.grid[0][x] = 1 - if self.n_dots < self.n: + class FallingDots(FacePlateAnimation): + def __init__(self, n=10, bus=None): + super().__init__(bus=bus) self._create = True + assert 0 < n < 32 + self.n = n - -class StraightParticleShooter(FacePlateAnimation): - def __init__(self, period=None, bus=None): - super().__init__(bus=bus) - self.direction = "d" - self.period = period - self.counter = 0 - # draw shooter - self.grid[0][0] = 1 - self.grid[1][0] = 1 - self.grid[1][1] = 1 - self.grid[2][0] = 1 - - def line_down(self): - old = copy.deepcopy(self.grid) - for y in range(self.height): - self.grid[y][0] = old[y - 1][0] - self.grid[y][1] = old[y - 1][1] - - def line_up(self): - old = copy.deepcopy(self.grid) - for y in range(self.height): - if y == self.height - 1: - self.grid[y][0] = old[0][0] - self.grid[y][1] = old[0][1] - else: - self.grid[y][0] = old[y + 1][0] - self.grid[y][1] = old[y + 1][1] - - def scroll_particles(self): - old = copy.deepcopy(self.grid) - for x in range(2, self.width): - for y in range(self.height): - if old[y][x] == 1: - self.grid[y][x] = 0 - if x < self.width - 1: - self.grid[y][x + 1] = 1 - - @property - def num_particles(self): - n = 0 - for x in range(2, self.width): + @property + def n_dots(self): + n = 0 for y in range(self.height): - if self.grid[y][x] == 1: - n += 1 - return n - - @property - def line(self): - for y in range(self.height): - if self.grid[y][0] == 1: - return y - return 0 - - def animate(self): - # collision detection - top_collision = False - bottom_collision = False - if self.grid[0][0] == 1: - top_collision = True - elif self.grid[self.height - 1][0] == 1: - bottom_collision = True - if top_collision: + for x in range(self.width): + if self.grid[y][x]: + n += 1 + return n + + def animate(self): + self.move_down() + if self._create: + if random.choice([True, False]): + self._create = False + x = random.randint(0, self.width - 1) + self.grid[0][x] = 1 + if self.n_dots < self.n: + self._create = True + + + class StraightParticleShooter(FacePlateAnimation): + def __init__(self, period=None, bus=None): + super().__init__(bus=bus) self.direction = "d" - elif bottom_collision: - self.direction = "u" - - # bounce the "emitter" up and down - if self.direction == "u": - self.line_up() - else: - self.line_down() - - # create particles - period = self.period or random.randint(0, 20) - if self.num_particles < 1 or self.counter >= period: - self.grid[self.line + 1][2] = 1 + self.period = period self.counter = 0 + # draw shooter + self.grid[0][0] = 1 + self.grid[1][0] = 1 + self.grid[1][1] = 1 + self.grid[2][0] = 1 + + def line_down(self): + old = copy.deepcopy(self.grid) + for y in range(self.height): + self.grid[y][0] = old[y - 1][0] + self.grid[y][1] = old[y - 1][1] - # animate particles - self.scroll_particles() - self.counter += 1 - - - -if __name__ == "__main__": - from ovos_utils.messagebus import get_mycroft_bus - from time import sleep + def line_up(self): + old = copy.deepcopy(self.grid) + for y in range(self.height): + if y == self.height - 1: + self.grid[y][0] = old[0][0] + self.grid[y][1] = old[0][1] + else: + self.grid[y][0] = old[y + 1][0] + self.grid[y][1] = old[y + 1][1] + + def scroll_particles(self): + old = copy.deepcopy(self.grid) + for x in range(2, self.width): + for y in range(self.height): + if old[y][x] == 1: + self.grid[y][x] = 0 + if x < self.width - 1: + self.grid[y][x + 1] = 1 + + @property + def num_particles(self): + n = 0 + for x in range(2, self.width): + for y in range(self.height): + if self.grid[y][x] == 1: + n += 1 + return n + + @property + def line(self): + for y in range(self.height): + if self.grid[y][0] == 1: + return y + return 0 + + def animate(self): + # collision detection + top_collision = False + bottom_collision = False + if self.grid[0][0] == 1: + top_collision = True + elif self.grid[self.height - 1][0] == 1: + bottom_collision = True + if top_collision: + self.direction = "d" + elif bottom_collision: + self.direction = "u" + + # bounce the "emitter" up and down + if self.direction == "u": + self.line_up() + else: + self.line_down() - bus = get_mycroft_bus("192.168.1.70") + # create particles + period = self.period or random.randint(0, 20) + if self.num_particles < 1 or self.counter >= period: + self.grid[self.line + 1][2] = 1 + self.counter = 0 - for faceplate in ParticleBox(bus=bus): - faceplate.display(invert=False) - sleep(0.5) \ No newline at end of file + # animate particles + self.scroll_particles() + self.counter += 1 diff --git a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py b/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py index d543e9fa..92d43245 100644 --- a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py +++ b/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py @@ -1,492 +1,485 @@ -from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation import copy import random +from ovos_utils.log import LOG -# Game of Life Base -class GoL(FacePlateAnimation): +LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" + " this module will be removed in version 0.1.0") - def __init__(self, entropy=0, grid=None, bus=None): - super().__init__(grid, bus) - self.entropy = entropy - if self.is_empty: - self.randomize() +try: + from ovos_mark1.faceplate.cellular_automaton import * +except: + from ovos_utils.enclosure.mark1.faceplate import FacePlateAnimation - def _live_neighbours(self, y, x): - """Returns the number of live neighbours.""" - count = 0 - if y > 0: - if self.grid[y - 1][x]: - count = count + 1 - if x > 0: - if self.grid[y - 1][x - 1]: - count = count + 1 - if self.width > (x + 1): - if self.grid[y - 1][x + 1]: - count = count + 1 - if x > 0: - if self.grid[y][x - 1]: - count = count + 1 - if self.width > (x + 1): - if self.grid[y][x + 1]: - count = count + 1 + # Game of Life Base + class GoL(FacePlateAnimation): + + def __init__(self, entropy=0, grid=None, bus=None): + super().__init__(grid, bus) + self.entropy = entropy + if self.is_empty: + self.randomize() + + def _live_neighbours(self, y, x): + """Returns the number of live neighbours.""" + count = 0 + if y > 0: + if self.grid[y - 1][x]: + count = count + 1 + if x > 0: + if self.grid[y - 1][x - 1]: + count = count + 1 + if self.width > (x + 1): + if self.grid[y - 1][x + 1]: + count = count + 1 - if self.height > (y + 1): - if self.grid[y + 1][x]: - count = count + 1 if x > 0: - if self.grid[y + 1][x - 1]: + if self.grid[y][x - 1]: count = count + 1 if self.width > (x + 1): - if self.grid[y + 1][x + 1]: + if self.grid[y][x + 1]: count = count + 1 - return count - - def animate(self): - """Game of Life turn""" - nt = copy.deepcopy(self.grid) - for y in range(0, self.height): - for x in range(0, self.width): - neighbours = self._live_neighbours(y, x) - if self.grid[y][x] == 0: - if neighbours == 3: - nt[y][x] = 1 - else: - if (neighbours < 2) or (neighbours > 3): - nt[y][x] = 0 - if nt == self.grid and self.entropy <= 0: - self.stop() - self.grid = nt - self.randomize(self.entropy) - if self.is_empty: - self.stop() - - -# Langtons Ant base -class _Ant: - def __init__(self, x, y, direction, height=8, width=32): - self.x = x - self.y = y - assert direction[0] in ["r", "l", "u", "d"] - self.direction = direction[0] - self.grid_height = height - self.grid_width = width - self.dead = False - - def move_forward(self): - if self.direction == "r": - self.x += 1 - if self.x == self.grid_width: - self.dead = True - elif self.direction == "d": - self.y += 1 - if self.y == self.grid_height: - self.dead = True - elif self.direction == "l": - self.x -= 1 - if self.x == -1: - self.dead = True - elif self.direction == "u": - self.y -= 1 - if self.y == -1: - self.dead = True - - def turn_right(self): - if self.direction == "r": - self.direction = "d" - elif self.direction == "d": - self.direction = "l" - elif self.direction == "l": - self.direction = "u" - elif self.direction == "u": - self.direction = "r" - - def turn_left(self): - if self.direction == "r": - self.direction = "u" - elif self.direction == "d": - self.direction = "r" - elif self.direction == "l": - self.direction = "d" - elif self.direction == "u": - self.direction = "l" - - -class _InfiniteAnt(_Ant): - def move_forward(self): - if self.direction == "r": - self.x += 1 - if self.x == self.grid_width: - self.x = 0 - elif self.direction == "d": - self.y += 1 - if self.y == self.grid_height: - self.y = 0 - elif self.direction == "l": - self.x -= 1 - if self.x == -1: - self.x = self.grid_width - 1 - elif self.direction == "u": - self.y -= 1 - if self.y == -1: - self.y = self.grid_height - 1 - - -class _ReverseAnt(_Ant): - def turn_left(self): - super().turn_right() - - def turn_right(self): - super().turn_left() - - -class _ReverseInfiniteAnt(_InfiniteAnt): - def turn_left(self): - super().turn_right() - - def turn_right(self): - super().turn_left() - - -class LangtonsAnt(FacePlateAnimation): - def __init__(self, ants=1, continuous=True, gen_reverse=False, - grid=None, bus=None): - super().__init__(grid=grid, bus=bus) - self.ants = [] - # if continuous loops around the board - # height + 1 -> 0 - # width + 1 -> 0 - # else ant is removed - self.continuous = continuous - if isinstance(ants, int): - # spawn N ants - assert 0 <= ants < 256 - for i in range(ants): - x = random.randint(0, self.width - 1) - y = random.randint(0, self.height - 1) - direction = random.choice(["u", "d", "l", "r"]) - reverse = False - if gen_reverse: - reverse = random.choice([True, False]) - ant = self.ant_factory(x, y, direction, reverse) - self.ants.append(ant) - elif isinstance(ants, list): - # Ant objects - self.ants = ants - for ant in self.ants: - assert isinstance(ant, _Ant) - else: - raise ValueError - - def ant_factory(self, x, y, direction, reverse=False): - # reverse ants exit black squares to the opposite direction - if self.continuous: - # loops around the board instead of dying + if self.height > (y + 1): + if self.grid[y + 1][x]: + count = count + 1 + if x > 0: + if self.grid[y + 1][x - 1]: + count = count + 1 + if self.width > (x + 1): + if self.grid[y + 1][x + 1]: + count = count + 1 + + return count + + def animate(self): + """Game of Life turn""" + nt = copy.deepcopy(self.grid) + for y in range(0, self.height): + for x in range(0, self.width): + neighbours = self._live_neighbours(y, x) + if self.grid[y][x] == 0: + if neighbours == 3: + nt[y][x] = 1 + else: + if (neighbours < 2) or (neighbours > 3): + nt[y][x] = 0 + if nt == self.grid and self.entropy <= 0: + self.stop() + self.grid = nt + self.randomize(self.entropy) + if self.is_empty: + self.stop() + + + # Langtons Ant base + class _Ant: + def __init__(self, x, y, direction, height=8, width=32): + self.x = x + self.y = y + assert direction[0] in ["r", "l", "u", "d"] + self.direction = direction[0] + self.grid_height = height + self.grid_width = width + self.dead = False + + def move_forward(self): + if self.direction == "r": + self.x += 1 + if self.x == self.grid_width: + self.dead = True + elif self.direction == "d": + self.y += 1 + if self.y == self.grid_height: + self.dead = True + elif self.direction == "l": + self.x -= 1 + if self.x == -1: + self.dead = True + elif self.direction == "u": + self.y -= 1 + if self.y == -1: + self.dead = True + + def turn_right(self): + if self.direction == "r": + self.direction = "d" + elif self.direction == "d": + self.direction = "l" + elif self.direction == "l": + self.direction = "u" + elif self.direction == "u": + self.direction = "r" + + def turn_left(self): + if self.direction == "r": + self.direction = "u" + elif self.direction == "d": + self.direction = "r" + elif self.direction == "l": + self.direction = "d" + elif self.direction == "u": + self.direction = "l" + + + class _InfiniteAnt(_Ant): + def move_forward(self): + if self.direction == "r": + self.x += 1 + if self.x == self.grid_width: + self.x = 0 + elif self.direction == "d": + self.y += 1 + if self.y == self.grid_height: + self.y = 0 + elif self.direction == "l": + self.x -= 1 + if self.x == -1: + self.x = self.grid_width - 1 + elif self.direction == "u": + self.y -= 1 + if self.y == -1: + self.y = self.grid_height - 1 + + + class _ReverseAnt(_Ant): + def turn_left(self): + super().turn_right() + + def turn_right(self): + super().turn_left() + + + class _ReverseInfiniteAnt(_InfiniteAnt): + def turn_left(self): + super().turn_right() + + def turn_right(self): + super().turn_left() + + + class LangtonsAnt(FacePlateAnimation): + def __init__(self, ants=1, continuous=True, gen_reverse=False, + grid=None, bus=None): + super().__init__(grid=grid, bus=bus) + self.ants = [] + # if continuous loops around the board + # height + 1 -> 0 + # width + 1 -> 0 + # else ant is removed + self.continuous = continuous + if isinstance(ants, int): + # spawn N ants + assert 0 <= ants < 256 + for i in range(ants): + x = random.randint(0, self.width - 1) + y = random.randint(0, self.height - 1) + direction = random.choice(["u", "d", "l", "r"]) + reverse = False + if gen_reverse: + reverse = random.choice([True, False]) + ant = self.ant_factory(x, y, direction, reverse) + self.ants.append(ant) + elif isinstance(ants, list): + # Ant objects + self.ants = ants + for ant in self.ants: + assert isinstance(ant, _Ant) + else: + raise ValueError + + def ant_factory(self, x, y, direction, reverse=False): + # reverse ants exit black squares to the opposite direction + if self.continuous: + # loops around the board instead of dying + if reverse: + return _ReverseInfiniteAnt(x, y, direction) + return _InfiniteAnt(x, y, direction) if reverse: - return _ReverseInfiniteAnt(x, y, direction) - return _InfiniteAnt(x, y, direction) - if reverse: - return _ReverseAnt(x, y, direction) - return _Ant(x, y, direction) - - def move_ants(self): - # copy grid, multiple ants might want to flip same square - # end result is the same so this is not a problem as long as it does - # not change during iteration - old_grid = copy.deepcopy(self.grid) - - for idx, ant in enumerate(self.ants): - if self.ants[idx].dead: - continue - if old_grid[ant.y][ant.x] == 1: - # black - self.ants[idx].turn_left() + return _ReverseAnt(x, y, direction) + return _Ant(x, y, direction) + + def move_ants(self): + # copy grid, multiple ants might want to flip same square + # end result is the same so this is not a problem as long as it does + # not change during iteration + old_grid = copy.deepcopy(self.grid) + + for idx, ant in enumerate(self.ants): + if self.ants[idx].dead: + continue + if old_grid[ant.y][ant.x] == 1: + # black + self.ants[idx].turn_left() + else: + # white + self.ants[idx].turn_right() + # flip color + self.grid[ant.y][ant.x] = not old_grid[ant.y][ant.x] + # update ant position + self.ants[idx].move_forward() + + def animate(self): + self.move_ants() + # Stop condition -> all ants moved out of the board + dead_ants = [ant for ant in self.ants if ant.dead] + if len(dead_ants) == len(self.ants): + self.stop() + + + # Game of Life Animations + class SpaceInvader(GoL): + # This basically half "pulsar" + str_grid = """ + XXXXXXXXXXXX XXX XXXXXXXXXXXXX + XXXXXXXXXX X X X X X XXXXXXXXXXX + XXXXXXXX XX X XX XXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXX XXX XXXXXXXXXXXXX + XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX + XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + """ + + + # Langton's Ant animations + + # Single Ant + class LangtonsLineDisplacer(LangtonsAnt): + # see pattern here + # https://youtu.be/w6XQQhCgq5c?t=84 + + def __init__(self, x=None, y=None, continuous=True, bus=None): + super().__init__(0, continuous, bus=bus) + x = x if x is not None else random.randint(0, self.width - 1) + y = y if y is not None else random.randint(0, self.height - 1) + ant = self.ant_factory(x, y - 1, "u") + self.ants.append(ant) + # create initial line + for i in range(0, self.width): + self.grid[y][i] = 1 + + + # 2 Ants + class LangtonsAntsOscillator(LangtonsAnt): + # see pattern here + # https://youtu.be/w6XQQhCgq5c?t=103 + + def __init__(self, x=None, y=None, bus=None): + super().__init__(0, bus=bus) + x1 = x2 = x if x is not None else self.width // 2 + y1 = y if y is not None else self.height // 2 + y2 = y1 - 1 + dir1 = "d" + dir2 = "u" + ant1 = self.ant_factory(x1, y1, dir1) + ant2 = self.ant_factory(x2, y2, dir2) + self.ants += [ant1, ant2] + + + class LangtonsAntsOscillator2(LangtonsAnt): + def __init__(self, x=None, y=None, bus=None): + super().__init__(0, bus=bus) + x1 = x2 = x if x is not None else self.width // 2 + y1 = y if y is not None else self.height // 2 + y2 = y1 - 1 + dir1 = dir2 = "u" + ant1 = self.ant_factory(x1, y1, dir1) + ant2 = self.ant_factory(x2, y2, dir2) + self.ants += [ant1, ant2] + + + class LangtonsAntsOscillator3(LangtonsAnt): + def __init__(self, x=None, y=None, bus=None): + super().__init__(0, bus=bus) + x1 = x2 = x if x is not None else self.width // 2 + y1 = y if y is not None else self.height // 2 + y2 = y1 - 1 + dir1 = "l" + dir2 = "r" + ant1 = self.ant_factory(x1, y1, dir1) + ant2 = self.ant_factory(x2, y2, dir2) + self.ants += [ant1, ant2] + + + class LangtonsAntsOscillator4(LangtonsAnt): + def __init__(self, x=None, y=None, bus=None): + super().__init__(0, bus=bus) + x1 = x2 = x if x is not None else self.width // 2 + y1 = y if y is not None else self.height // 2 + y2 = y1 - 1 + dir1 = dir2 = "l" + ant1 = self.ant_factory(x1, y1, dir1) + ant2 = self.ant_factory(x2, y2, dir2) + self.ants += [ant1, ant2] + + + class LangtonsAntsOscillator5(LangtonsAnt): + def __init__(self, x=None, y=None, bus=None): + super().__init__(0, bus=bus) + x1 = x2 = x if x is not None else self.width // 2 + y1 = y if y is not None else self.height // 2 + y2 = y1 - 1 + dir1 = "u" + dir2 = "d" + ant1 = self.ant_factory(x1, y1, dir1) + ant2 = self.ant_factory(x2, y2, dir2) + self.ants += [ant1, ant2] + + + class LangtonsAntTrail(LangtonsAnt): + # see pattern here + # https://youtu.be/w6XQQhCgq5c?t=159 + + def __init__(self, x=None, y=None, bus=None): + super().__init__(0, bus=bus) + x = x if x is not None else random.randint(0, self.width - 1) + y = y if y is not None else random.randint(0, self.height - 1) + dir1 = "u" + dir2 = "d" + ant1 = self.ant_factory(x, y - 1, dir1) + ant2 = self.ant_factory(x, y, dir2, reverse=True) + self.ants += [ant1, ant2] + + + class LangtonsAntDotTransporter(LangtonsAnt): + # https://youtu.be/w6XQQhCgq5c?t=171 + + def __init__(self, x=None, y=None, bus=None): + super().__init__(0, bus=bus) + x = x if x is not None else random.randint(0, self.width - 1) + y = y if y is not None else random.randint(0, self.height - 1) + dir1 = dir2 = "u" + ant1 = self.ant_factory(x, y, dir1) + ant2 = self.ant_factory(x + 1, y, dir2, reverse=True) + self.ants += [ant1, ant2] + # initial grid + if y + 1 == self.height: + self.grid[0][x + 1] = 1 # bellow anti-ant else: - # white - self.ants[idx].turn_right() - # flip color - self.grid[ant.y][ant.x] = not old_grid[ant.y][ant.x] - # update ant position - self.ants[idx].move_forward() - - def animate(self): - self.move_ants() - # Stop condition -> all ants moved out of the board - dead_ants = [ant for ant in self.ants if ant.dead] - if len(dead_ants) == len(self.ants): - self.stop() - - -# Game of Life Animations -class SpaceInvader(GoL): - # This basically half "pulsar" - str_grid = """ -XXXXXXXXXXXX XXX XXXXXXXXXXXXX -XXXXXXXXXX X X X X X XXXXXXXXXXX -XXXXXXXX XX X XX XXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXX XXX XXXXXXXXXXXXX -XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX -XXXXXXXXXXXX XXXXX XXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -""" - - -# Langton's Ant animations - -# Single Ant -class LangtonsLineDisplacer(LangtonsAnt): - # see pattern here - # https://youtu.be/w6XQQhCgq5c?t=84 - - def __init__(self, x=None, y=None, continuous=True, bus=None): - super().__init__(0, continuous, bus=bus) - x = x if x is not None else random.randint(0, self.width - 1) - y = y if y is not None else random.randint(0, self.height - 1) - ant = self.ant_factory(x, y - 1, "u") - self.ants.append(ant) - # create initial line - for i in range(0, self.width): - self.grid[y][i] = 1 - - -# 2 Ants -class LangtonsAntsOscillator(LangtonsAnt): - # see pattern here - # https://youtu.be/w6XQQhCgq5c?t=103 - - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = "d" - dir2 = "u" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - -class LangtonsAntsOscillator2(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = dir2 = "u" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - -class LangtonsAntsOscillator3(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = "l" - dir2 = "r" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - -class LangtonsAntsOscillator4(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = dir2 = "l" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - -class LangtonsAntsOscillator5(LangtonsAnt): - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x1 = x2 = x if x is not None else self.width // 2 - y1 = y if y is not None else self.height // 2 - y2 = y1 - 1 - dir1 = "u" - dir2 = "d" - ant1 = self.ant_factory(x1, y1, dir1) - ant2 = self.ant_factory(x2, y2, dir2) - self.ants += [ant1, ant2] - - -class LangtonsAntTrail(LangtonsAnt): - # see pattern here - # https://youtu.be/w6XQQhCgq5c?t=159 - - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x = x if x is not None else random.randint(0, self.width - 1) - y = y if y is not None else random.randint(0, self.height - 1) - dir1 = "u" - dir2 = "d" - ant1 = self.ant_factory(x, y - 1, dir1) - ant2 = self.ant_factory(x, y, dir2, reverse=True) - self.ants += [ant1, ant2] - - -class LangtonsAntDotTransporter(LangtonsAnt): - # https://youtu.be/w6XQQhCgq5c?t=171 - - def __init__(self, x=None, y=None, bus=None): - super().__init__(0, bus=bus) - x = x if x is not None else random.randint(0, self.width - 1) - y = y if y is not None else random.randint(0, self.height - 1) - dir1 = dir2 = "u" - ant1 = self.ant_factory(x, y, dir1) - ant2 = self.ant_factory(x + 1, y, dir2, reverse=True) - self.ants += [ant1, ant2] - # initial grid - if y + 1 == self.height: - self.grid[0][x + 1] = 1 # bellow anti-ant - else: - self.grid[y + 1][x + 1] = 1 # bellow anti-ant - self.grid[y - 1][x] = 1 # above ant - - -# 1D / elementar automata - -class ElementarAutomata(FacePlateAnimation): - def __init__(self, direction="u", idx=0, seed=None, grid=None, bus=None): - super().__init__(grid, bus) - assert direction[0] in ["u", "d", "l", "r"] - self.direction = direction[0] - self.row = idx - self.initial_state(seed) - - def initial_state(self, seed=None): - if seed is not None: - line = seed - else: - line = [0 for i in range(self.width)] - self.grid[self.row] = line - - def rule(self): - # process the line - raise NotImplementedError - - def animate(self): - old = copy.deepcopy(self.grid) - new_line = self.rule() - if self.direction == "u": - self.move_up() - elif self.direction == "d": - self.move_down() - elif self.direction == "l": - self.move_left() - elif self.direction == "r": - self.move_right() - self.grid[self.row] = new_line - if old == self.grid: - self.stop() - - -class SierpinskiTriangle(ElementarAutomata): - def __init__(self, direction="u", seed=None, bus=None): - assert direction[0] in ["u", "d"] - if direction[0] == "u": - idx = -1 - else: - idx = 0 - super().__init__(direction, idx, seed=seed, bus=bus) - - def initial_state(self, seed=None): - if seed is not None: - line = seed - else: - line = [0 for i in range(self.width)] - idx = self.width // 2 - line[idx] = 1 - self.grid[self.row] = line - - def rule(self): - new_line = copy.deepcopy(self.grid[self.row]) - # handle middle - for i in range(1, self.width - 1): - left = self.grid[self.row][i - 1] - right = self.grid[self.row][i + 1] - if left and right: - new_line[i] = 0 - elif left or right: - new_line[i] = 1 + self.grid[y + 1][x + 1] = 1 # bellow anti-ant + self.grid[y - 1][x] = 1 # above ant + + + # 1D / elementar automata + + class ElementarAutomata(FacePlateAnimation): + def __init__(self, direction="u", idx=0, seed=None, grid=None, bus=None): + super().__init__(grid, bus) + assert direction[0] in ["u", "d", "l", "r"] + self.direction = direction[0] + self.row = idx + self.initial_state(seed) + + def initial_state(self, seed=None): + if seed is not None: + line = seed + else: + line = [0 for i in range(self.width)] + self.grid[self.row] = line + + def rule(self): + # process the line + raise NotImplementedError + + def animate(self): + old = copy.deepcopy(self.grid) + new_line = self.rule() + if self.direction == "u": + self.move_up() + elif self.direction == "d": + self.move_down() + elif self.direction == "l": + self.move_left() + elif self.direction == "r": + self.move_right() + self.grid[self.row] = new_line + if old == self.grid: + self.stop() + + + class SierpinskiTriangle(ElementarAutomata): + def __init__(self, direction="u", seed=None, bus=None): + assert direction[0] in ["u", "d"] + if direction[0] == "u": + idx = -1 + else: + idx = 0 + super().__init__(direction, idx, seed=seed, bus=bus) + + def initial_state(self, seed=None): + if seed is not None: + line = seed + else: + line = [0 for i in range(self.width)] + idx = self.width // 2 + line[idx] = 1 + self.grid[self.row] = line + + def rule(self): + new_line = copy.deepcopy(self.grid[self.row]) + # handle middle + for i in range(1, self.width - 1): + left = self.grid[self.row][i - 1] + right = self.grid[self.row][i + 1] + if left and right: + new_line[i] = 0 + elif left or right: + new_line[i] = 1 + else: + new_line[i] = 0 + + # handle edges + # if self.grid[0][1] == 1: + # new_line[0] = 1 + # else: + # new_line[0] = 0 + # if self.grid[0][-2] == 1: + # new_line[-1] = 1 + # else: + # new_line[-1] = 0 + return new_line + + + class Rule110(ElementarAutomata): + def __init__(self, direction="u", seed=None, bus=None): + assert direction[0] in ["u", "d"] + if direction[0] == "u": + idx = -1 + else: + idx = 0 + super().__init__(direction, idx, seed=seed, bus=bus) + + def initial_state(self, seed=None): + if seed is not None: + line = seed + else: + line = [0 for i in range(self.width)] + idx = self.width - 1 + line[idx] = 1 + self.grid[self.row] = line + + def rule(self): + new_line = copy.deepcopy(self.grid[self.row]) + # handle middle + for i in range(1, self.width - 1): + left = self.grid[self.row][i - 1] + mid = self.grid[self.row][i] + right = self.grid[self.row][i + 1] + if str(left) + str(mid) + str(right) in \ + ["110", "101", "011", "010", "001"]: + new_line[i] = 1 + else: + new_line[i] = 0 + + # handle edges + if self.grid[0][1] == 1: + new_line[0] = 1 else: - new_line[i] = 0 - - # handle edges - #if self.grid[0][1] == 1: - # new_line[0] = 1 - #else: - # new_line[0] = 0 - #if self.grid[0][-2] == 1: - # new_line[-1] = 1 - #else: - # new_line[-1] = 0 - return new_line - - -class Rule110(ElementarAutomata): - def __init__(self, direction="u", seed=None, bus=None): - assert direction[0] in ["u", "d"] - if direction[0] == "u": - idx = -1 - else: - idx = 0 - super().__init__(direction, idx, seed=seed, bus=bus) - - def initial_state(self, seed=None): - if seed is not None: - line = seed - else: - line = [0 for i in range(self.width)] - idx = self.width - 1 - line[idx] = 1 - self.grid[self.row] = line - - def rule(self): - new_line = copy.deepcopy(self.grid[self.row]) - # handle middle - for i in range(1, self.width - 1): - left = self.grid[self.row][i - 1] - mid = self.grid[self.row][i] - right = self.grid[self.row][i + 1] - if str(left) + str(mid) + str(right) in \ - ["110", "101", "011", "010", "001"]: - new_line[i] = 1 + new_line[0] = 0 + if self.grid[0][-2] == 1: + new_line[-1] = 1 else: - new_line[i] = 0 - - # handle edges - if self.grid[0][1] == 1: - new_line[0] = 1 - else: - new_line[0] = 0 - if self.grid[0][-2] == 1: - new_line[-1] = 1 - else: - new_line[-1] = 0 - return new_line - - -if __name__ == "__main__": - from time import sleep - from ovos_utils.messagebus import get_mycroft_bus - from time import sleep - - bus = get_mycroft_bus("192.168.1.70") - - a = Rule110(bus=bus) - a.print() - - for grid in a: - grid.print() - grid.display(invert=False) - sleep(0.5) \ No newline at end of file + new_line[-1] = 0 + return new_line diff --git a/ovos_utils/enclosure/mark1/faceplate/icons.py b/ovos_utils/enclosure/mark1/faceplate/icons.py index 7def1227..3db4ae83 100644 --- a/ovos_utils/enclosure/mark1/faceplate/icons.py +++ b/ovos_utils/enclosure/mark1/faceplate/icons.py @@ -1,235 +1,239 @@ -from ovos_utils.enclosure.mark1.faceplate import FaceplateGrid, BlackScreen - - -# drawing in python - - -class MusicIcon(BlackScreen): - str_grid = """ -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXX -XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX -XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX -XXXXXXXXXXXXX XX XXXXXXXXXXXXX -XXXXXXXXXXXX X XXXXXXXXXXXXX -XXXXXXXXXXXXX XXX XXXXXXXXXXXXXX -""" - - -class PlusIcon(BlackScreen): - str_grid = """ -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -""" - - -class HeartIcon(FaceplateGrid): - str_grid = """ - - xx xx - xxxx xxxx - xxxxxxxxx - xxxxxxx - xxxxx - xxx - x -""" - - -class HollowHeartIcon(FaceplateGrid): - str_grid = """ - - xx xx - x x x x - x x x - x x - x x - x x - x -""" - - -class SkullIcon(FaceplateGrid): - str_grid = """ - - xxxxxxx - x xxx x - xxxxxxxxx - xxx xxx - xxxxx - x x x - x x x -""" - - -class DeadFishIcon(FaceplateGrid): - str_grid = """ - - x xxxx - x x x x xx xxx - xxxxxxxxxxxxxxx - x x x x xxxxxx - x xxxx -""" - - -class InfoIcon(BlackScreen): - str_grid = """ -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXX -""" - - -class ArrowLeftIcon(BlackScreen): - str_grid = """ -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXXXXXXX -XXXXXXXXXXXX XXXXXXXXXXXXXXXXX -XXXXXXXXXXX X XXXXXXXXXX -XXXXXXXXXX X XXXXXXXXXX -XXXXXXXXXXX X XXXXXXXXXX -XXXXXXXXXXXX XXXXXXXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXXXXXXX -""" - - -class WarningIcon(BlackScreen): - str_grid = """ -XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXXXX -XXXXXXXXXXXXX X XXXXXXXXXXXXXX -XXXXXXXXXXXX XXX XXXXXXXXXXXXX -XXXXXXXXXXX XXX XXXXXXXXXXXX -XXXXXXXXXX XXXXXXXXXXX -XXXXXXXXX XXX XXXXXXXXXX -XXXXXXXX X XXXXXXXXX -""" - - -class CrossIcon(BlackScreen): - str_grid = """ -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX -XXXXXXXXXXXX XXX XXXXXXXXXXX -XXXXXXXXXXXXX X XXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXX X XXXXXXXXXXXX -XXXXXXXXXXXX XXX XXXXXXXXXXX -XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX -""" - - -class JarbasAI(BlackScreen): - str_grid = """ -X XXXXXXXXXXXXXXXXXXXXXXX X -XX XXXXXXXXXXXXXXXXXXXXXXXX X XX -XX XXXXXXXXXX XXXXXXXXXXXXX X X -XX XXXXXXXXXX XXXXXXXXX X X -XX XX X X X XX X XXX X X - X X XX X XX XX X XX X X X X - X X XX X XXX XX X XX XXX X X X - X X XXX X X X X X -""" - - -class SpaceInvader1(BlackScreen): - str_grid = """ -XXXXXXXXXXXXXX XXXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXX x x XXXXXXXXX -XXXXXXXXXX XX XX XXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXX -XXXXXXXXXX XXXXXXXXX -XXXXXXXXXX XXXXXXXXXXX XXXXXXXXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -""" - - -class SpaceInvader2(BlackScreen): - str_grid = """ -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXXXX x x XXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXX X X XXXXXXXXX -XXXXXXXXXX XXXXXXXXX -XXXXXXXXXX XXX XXX XXXXXXXXX -XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX -XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX -""" - - -class SpaceInvader3(BlackScreen): - str_grid = """ -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXX -XXXXXXXXXXXX X X XXXXXXXXXXX -XXXXXXXXXX XXXXXXXXX -XXXXXXXXXX XX XXX XX XXXXXXXXX -XXXXXXXXXXXXXX XXXXXXXXXXXXX -XXXXXXXXXXXXX XXX XXXXXXXXXXXX -""" - - -class SpaceInvader4(BlackScreen): - str_grid = """ -XXXXXXXXXXXXX XXXXXXXXXXXX -XXXXXXXXXXXXXXX XXXXXXXXXXXXXX -XXXXXXXXXXXXX XXXXXXXXXXX -XXXXXXXXXXXX X XXXXXXXXXXX -XXXXXXXXXX XXXXXXXXX -XXXXXXXXXX XX XX XXXXXXXXX -XXXXXXXXXXXXXX X XXXXXXXXXXXXX -XXXXXXXXXXXXX XXX XXXXXXXXXXXX -""" - - -# Encoded icons -class Boat(BlackScreen): - encoded = "QIAAABACAGIEMEOEPHAEAGACABABAAAAAA" - - -# Default weather icons for mark1 -class SunnyIcon(FaceplateGrid): - encoded = "IICEIBMDNLMDIBCEAA" - - -class PartlyCloudyIcon(FaceplateGrid): - encoded = "IIEEGBGDHLHDHBGEEA" - +from ovos_utils.log import LOG + +LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" + " this module will be removed in version 0.1.0") + +try: + from ovos_mark1.faceplate.icons import * +except: + from ovos_utils.enclosure.mark1.faceplate import FaceplateGrid, BlackScreen + + class MusicIcon(BlackScreen): + str_grid = """ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXX XXXXXXXXXXXXX + XXXXXXXXXXXXXX XXXXXXXXXXXXX + XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX + XXXXXXXXXXXXXX XXX XXXXXXXXXXXXX + XXXXXXXXXXXXX XX XXXXXXXXXXXXX + XXXXXXXXXXXX X XXXXXXXXXXXXX + XXXXXXXXXXXXX XXX XXXXXXXXXXXXXX + """ + + + class PlusIcon(BlackScreen): + str_grid = """ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + """ + + + class HeartIcon(FaceplateGrid): + str_grid = """ + + xx xx + xxxx xxxx + xxxxxxxxx + xxxxxxx + xxxxx + xxx + x + """ + + + class HollowHeartIcon(FaceplateGrid): + str_grid = """ + + xx xx + x x x x + x x x + x x + x x + x x + x + """ + + + class SkullIcon(FaceplateGrid): + str_grid = """ + + xxxxxxx + x xxx x + xxxxxxxxx + xxx xxx + xxxxx + x x x + x x x + """ + + + class DeadFishIcon(FaceplateGrid): + str_grid = """ + + x xxxx + x x x x xx xxx + xxxxxxxxxxxxxxx + x x x x xxxxxx + x xxxx + """ + + + class InfoIcon(BlackScreen): + str_grid = """ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXX + """ + + + class ArrowLeftIcon(BlackScreen): + str_grid = """ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXXXXXXX + XXXXXXXXXXXX XXXXXXXXXXXXXXXXX + XXXXXXXXXXX X XXXXXXXXXX + XXXXXXXXXX X XXXXXXXXXX + XXXXXXXXXXX X XXXXXXXXXX + XXXXXXXXXXXX XXXXXXXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXXXXXXX + """ + + + class WarningIcon(BlackScreen): + str_grid = """ + XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX + XXXXXXXXXXXXXX XXXXXXXXXXXXXXX + XXXXXXXXXXXXX X XXXXXXXXXXXXXX + XXXXXXXXXXXX XXX XXXXXXXXXXXXX + XXXXXXXXXXX XXX XXXXXXXXXXXX + XXXXXXXXXX XXXXXXXXXXX + XXXXXXXXX XXX XXXXXXXXXX + XXXXXXXX X XXXXXXXXX + """ + + + class CrossIcon(BlackScreen): + str_grid = """ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX + XXXXXXXXXXXX XXX XXXXXXXXXXX + XXXXXXXXXXXXX X XXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXX X XXXXXXXXXXXX + XXXXXXXXXXXX XXX XXXXXXXXXXX + XXXXXXXXXXXXX XXXXX XXXXXXXXXXXX + """ + + + class JarbasAI(BlackScreen): + str_grid = """ + X XXXXXXXXXXXXXXXXXXXXXXX X + XX XXXXXXXXXXXXXXXXXXXXXXXX X XX + XX XXXXXXXXXX XXXXXXXXXXXXX X X + XX XXXXXXXXXX XXXXXXXXX X X + XX XX X X X XX X XXX X X + X X XX X XX XX X XX X X X X + X X XX X XXX XX X XX XXX X X X + X X XXX X X X X X + """ + + + class SpaceInvader1(BlackScreen): + str_grid = """ + XXXXXXXXXXXXXX XXXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXX x x XXXXXXXXX + XXXXXXXXXX XX XX XXXXXXXXX + XXXXXXXXXXXXXX XXXXXXXXXXXXX + XXXXXXXXXX XXXXXXXXX + XXXXXXXXXX XXXXXXXXXXX XXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + """ + + + class SpaceInvader2(BlackScreen): + str_grid = """ + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXXXX x x XXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXX X X XXXXXXXXX + XXXXXXXXXX XXXXXXXXX + XXXXXXXXXX XXX XXX XXXXXXXXX + XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX + XXXXXXXXXXXXXXX X XXXXXXXXXXXXXX + """ + + + class SpaceInvader3(BlackScreen): + str_grid = """ + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXX + XXXXXXXXXXXX X X XXXXXXXXXXX + XXXXXXXXXX XXXXXXXXX + XXXXXXXXXX XX XXX XX XXXXXXXXX + XXXXXXXXXXXXXX XXXXXXXXXXXXX + XXXXXXXXXXXXX XXX XXXXXXXXXXXX + """ + + + class SpaceInvader4(BlackScreen): + str_grid = """ + XXXXXXXXXXXXX XXXXXXXXXXXX + XXXXXXXXXXXXXXX XXXXXXXXXXXXXX + XXXXXXXXXXXXX XXXXXXXXXXX + XXXXXXXXXXXX X XXXXXXXXXXX + XXXXXXXXXX XXXXXXXXX + XXXXXXXXXX XX XX XXXXXXXXX + XXXXXXXXXXXXXX X XXXXXXXXXXXXX + XXXXXXXXXXXXX XXX XXXXXXXXXXXX + """ + + + # Encoded icons + class Boat(BlackScreen): + encoded = "QIAAABACAGIEMEOEPHAEAGACABABAAAAAA" + + + # Default weather icons for mark1 + class SunnyIcon(FaceplateGrid): + encoded = "IICEIBMDNLMDIBCEAA" + + + class PartlyCloudyIcon(FaceplateGrid): + encoded = "IIEEGBGDHLHDHBGEEA" + -class CloudyIcon(FaceplateGrid): - encoded = "IIIBMDMDODODODMDIB" + class CloudyIcon(FaceplateGrid): + encoded = "IIIBMDMDODODODMDIB" -class LightRainIcon(FaceplateGrid): - encoded = "IIMAOJOFPBPJPFOBMA" + class LightRainIcon(FaceplateGrid): + encoded = "IIMAOJOFPBPJPFOBMA" -class RainIcon(FaceplateGrid): - encoded = "IIMIOFOBPFPDPJOFMA" + class RainIcon(FaceplateGrid): + encoded = "IIMIOFOBPFPDPJOFMA" -class StormIcon(FaceplateGrid): - encoded = "IIAAIIMEODLBJAAAAA" + class StormIcon(FaceplateGrid): + encoded = "IIAAIIMEODLBJAAAAA" -class SnowIcon(FaceplateGrid): - encoded = "IIJEKCMBPHMBKCJEAA" + class SnowIcon(FaceplateGrid): + encoded = "IIJEKCMBPHMBKCJEAA" -class WindIcon(FaceplateGrid): - encoded = "IIABIBIBIJIJJGJAGA" + class WindIcon(FaceplateGrid): + encoded = "IIABIBIBIJIJJGJAGA" diff --git a/ovos_utils/events.py b/ovos_utils/events.py index 74063831..b8d7ca68 100644 --- a/ovos_utils/events.py +++ b/ovos_utils/events.py @@ -2,12 +2,13 @@ from datetime import datetime, timedelta from inspect import signature from typing import Callable, Optional, Union + +import ovos_utils.messagebus from ovos_utils.intents.intent_service_interface import to_alnum from ovos_utils.log import LOG, log_deprecation, deprecated -from ovos_utils.messagebus import FakeBus, FakeMessage as Message, dig_for_message -def unmunge_message(message: Message, skill_id: str) -> Message: +def unmunge_message(message, skill_id: str): """ Restore message keywords by removing the Letterified skill ID. Args: @@ -16,7 +17,8 @@ def unmunge_message(message: Message, skill_id: str) -> Message: Returns: Message without clear keywords """ - if isinstance(message, Message) and isinstance(message.data, dict): + if isinstance(message, ovos_utils.messagebus.Message) and \ + isinstance(message.data, dict): skill_id = to_alnum(skill_id) for key in list(message.data.keys()): if key.startswith(skill_id): @@ -45,10 +47,10 @@ def get_handler_name(handler: Callable) -> str: def create_wrapper(handler: Callable[..., None], skill_id: str, - on_start: Callable[[Message], None], - on_end: Callable[[Message], None], + on_start: Callable[..., None], + on_end: Callable[..., None], on_error: Callable[..., None]) \ - -> Callable[[Message], None]: + -> Callable[..., None]: """ Create the default skill handler wrapper. This wrapper handles things like metrics, reporting handler start/stop @@ -90,8 +92,8 @@ def wrapper(message): def create_basic_wrapper(handler: Callable[..., None], on_error: Optional[Callable[[Exception], - None]] = None) -> \ - Callable[[Message], None]: + None]] = None) -> \ + Callable[..., None]: """ Create the default skill handler wrapper. @@ -129,13 +131,13 @@ class EventContainer: """ def __init__(self, bus=None): - self.bus = bus or FakeBus() + self.bus = bus or ovos_utils.messagebus.FakeBus() self.events = [] def set_bus(self, bus): self.bus = bus - def add(self, name: str, handler: Callable[[Message], None], + def add(self, name: str, handler: Callable[..., None], once: bool = False): """ Create event handler for executing intent or other event. @@ -239,7 +241,7 @@ def set_id(self, sched_id: str): self.skill_id = sched_id def _get_source_message(self): - message = dig_for_message() or Message("") + message = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") message.context['skill_id'] = self.skill_id return message @@ -252,7 +254,7 @@ def _create_unique_name(self, name: str) -> str: # TODO: Is a null name valid or should it raise an exception? return self.skill_id + ':' + (name or '') - def _schedule_event(self, handler: Callable[[Optional[Message]], None], + def _schedule_event(self, handler: Callable[..., None], when: Union[datetime, int, float], data: Optional[dict], name: Optional[str], @@ -299,10 +301,10 @@ def on_error(e): message = self._get_source_message() context = context or message.context context["skill_id"] = self.skill_id - self.bus.emit(Message('mycroft.scheduler.schedule_event', - data=event_data, context=context)) + self.bus.emit(ovos_utils.messagebus.Message('mycroft.scheduler.schedule_event', + data=event_data, context=context)) - def schedule_event(self, handler: Callable[[Optional[Message]], None], + def schedule_event(self, handler: Callable[..., None], when: Union[datetime, int, float], data: Optional[dict] = None, name: Optional[str] = None, @@ -319,7 +321,7 @@ def schedule_event(self, handler: Callable[[Optional[Message]], None], self._schedule_event(handler, when, data, name, context=context) def schedule_repeating_event(self, - handler: Callable[[Optional[Message]], None], + handler: Callable[..., None], when: Optional[Union[datetime, int, float]], interval: Union[float, int], data: Optional[dict] = None, diff --git a/ovos_utils/intents/__init__.py b/ovos_utils/intents/__init__.py index 623f8fb0..2f14e69d 100644 --- a/ovos_utils/intents/__init__.py +++ b/ovos_utils/intents/__init__.py @@ -2,11 +2,14 @@ IntentServiceInterface from ovos_utils.intents.converse import ConverseTracker from ovos_utils.intents.layers import IntentLayers +from ovos_utils.log import LOG + +LOG.warning("ovos_utils.intents moved to ovos_workshop.intents") try: - from adapt.intent import IntentBuilder, Intent + from ovos_workshop.intents import * + except ImportError: - # adapt is optional, these classes are mainly syntactic sugar class Intent: def __init__(self, name, requires, at_least_one, optional): diff --git a/ovos_utils/intents/converse.py b/ovos_utils/intents/converse.py index 0d232cb1..05b545a1 100644 --- a/ovos_utils/intents/converse.py +++ b/ovos_utils/intents/converse.py @@ -1,8 +1,10 @@ import time +import ovos_utils.messagebus from ovos_utils.intents.intent_service_interface import IntentQueryApi from ovos_utils.log import LOG -from ovos_utils.messagebus import FakeMessage as Message + +LOG.warning("ConverseTracker has been deprecated without replacement, it will be removed in 0.1.0") class ConverseTracker: @@ -90,8 +92,8 @@ def remove_active_skill(cls, skill_id, silent=False): if skill[0] == skill_id: cls.active_skills.remove(skill) if not silent: - cls.bus.emit(Message("converse.skill.deactivated", - {"skill_id": skill[0]})) + cls.bus.emit(ovos_utils.messagebus.Message("converse.skill.deactivated", + {"skill_id": skill[0]})) @classmethod def add_active_skill(cls, skill_id): @@ -106,8 +108,8 @@ def add_active_skill(cls, skill_id): cls.active_skills.insert(0, [skill_id, time.time()]) # this might be sent more than once and it's perfectly fine # it's just a new info message not consumed anywhere by default - cls.bus.emit(Message("converse.skill.activated", - {"skill_id": skill_id})) + cls.bus.emit(ovos_utils.messagebus.Message("converse.skill.activated", + {"skill_id": skill_id})) else: LOG.warning('Skill ID was empty, won\'t add to list of ' 'active skills.') diff --git a/ovos_utils/intents/intent_service_interface.py b/ovos_utils/intents/intent_service_interface.py index 55b17b60..db6448b2 100644 --- a/ovos_utils/intents/intent_service_interface.py +++ b/ovos_utils/intents/intent_service_interface.py @@ -2,361 +2,380 @@ from threading import RLock from typing import List, Tuple, Optional -from ovos_utils.messagebus import get_mycroft_bus, Message, dig_for_message +import ovos_utils.messagebus from ovos_utils.log import LOG, log_deprecation +LOG.warning("ovos_utils.intents moved to ovos_workshop.intents") -def to_alnum(skill_id: str) -> str: - """ - Convert a skill id to only alphanumeric characters - Non-alphanumeric characters are converted to "_" - - Args: - skill_id (str): identifier to be converted - Returns: - (str) String of letters - """ - return ''.join(c if c.isalnum() else '_' for c in str(skill_id)) - - -def munge_regex(regex: str, skill_id: str) -> str: - """ - Insert skill id as letters into match groups. - - Args: - regex (str): regex string - skill_id (str): skill identifier - Returns: - (str) munged regex - """ - base = '(?P<' + to_alnum(skill_id) - return base.join(regex.split('(?P<')) - - -def munge_intent_parser(intent_parser, name, skill_id): - """ - Rename intent keywords to make them skill exclusive - This gives the intent parser an exclusive name in the - format :. The keywords are given unique - names in the format . - - The function will not munge instances that's already been - munged - - Args: - intent_parser: (IntentParser) object to update - name: (str) Skill name - skill_id: (int) skill identifier - """ - # Munge parser name - if not name.startswith(str(skill_id) + ':'): - intent_parser.name = str(skill_id) + ':' + name - else: - intent_parser.name = name - - # Munge keywords - skill_id = to_alnum(skill_id) - # Munge required keyword - reqs = [] - for i in intent_parser.requires: - if not i[0].startswith(skill_id): - kw = (skill_id + i[0], skill_id + i[0]) - reqs.append(kw) - else: - reqs.append(i) - intent_parser.requires = reqs - - # Munge optional keywords - opts = [] - for i in intent_parser.optional: - if not i[0].startswith(skill_id): - kw = (skill_id + i[0], skill_id + i[0]) - opts.append(kw) - else: - opts.append(i) - intent_parser.optional = opts - - # Munge at_least_one keywords - at_least_one = [] - for i in intent_parser.at_least_one: - element = [skill_id + e.replace(skill_id, '') for e in i] - at_least_one.append(tuple(element)) - intent_parser.at_least_one = at_least_one - -class IntentServiceInterface: - """ - Interface to communicate with the Mycroft intent service. - - This class wraps the messagebus interface of the intent service allowing - for easier interaction with the service. It wraps both the Adapt and - Padatious parts of the intent services. - """ - - def __init__(self, bus=None): - self._bus = bus - self.skill_id = self.__class__.__name__ - # TODO: Consider using properties with setters to prevent duplicates - self.registered_intents: List[Tuple[str, object]] = [] - self.detached_intents: List[Tuple[str, object]] = [] - self._iterator_lock = RLock() - - @property - def intent_names(self) -> List[str]: +try: + from ovos_workshop.intents import * +except: + def to_alnum(skill_id: str) -> str: """ - Get a list of intent names (both registered and disabled). - """ - return [a[0] for a in self.registered_intents + self.detached_intents] - - @property - def bus(self): - if not self._bus: - raise RuntimeError("bus not set. call `set_bus()` before trying to" - "interact with the Messagebus") - return self._bus + Convert a skill id to only alphanumeric characters + Non-alphanumeric characters are converted to "_" - @bus.setter - def bus(self, val): - self.set_bus(val) - - def set_bus(self, bus=None): - self._bus = bus or get_mycroft_bus() + Args: + skill_id (str): identifier to be converted + Returns: + (str) String of letters + """ + return ''.join(c if c.isalnum() else '_' for c in str(skill_id)) - def set_id(self, skill_id: str): - self.skill_id = skill_id - def register_adapt_keyword(self, vocab_type: str, entity: str, - aliases: Optional[List[str]] = None, - lang: str = None): + def munge_regex(regex: str, skill_id: str) -> str: """ - Send a message to the intent service to add an Adapt keyword. - @param vocab_type: Keyword reference (file basename) - @param entity: Primary keyword value - @param aliases: List of alternative keyword values - @param lang: BCP-47 language code of entity and aliases - """ - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - - # TODO 22.02: Remove compatibility data - aliases = aliases or [] - entity_data = {'entity_value': entity, - 'entity_type': vocab_type, - 'lang': lang} - compatibility_data = {'start': entity, 'end': vocab_type} - - self.bus.emit(msg.forward("register_vocab", - {**entity_data, **compatibility_data})) - for alias in aliases: - alias_data = { - 'entity_value': alias, - 'entity_type': vocab_type, - 'alias_of': entity, - 'lang': lang} - compatibility_data = {'start': alias, 'end': vocab_type} - self.bus.emit(msg.forward("register_vocab", - {**alias_data, **compatibility_data})) + Insert skill id as letters into match groups. - def register_adapt_regex(self, regex: str, lang: str = None): + Args: + regex (str): regex string + skill_id (str): skill identifier + Returns: + (str) munged regex """ - Register a regex string with the intent service. - @param regex: Regex to be registered; Adapt extracts keyword references - from named match group. - @param lang: BCP-47 language code of regex - """ - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward("register_vocab", - {'regex': regex, 'lang': lang})) + base = '(?P<' + to_alnum(skill_id) + return base.join(regex.split('(?P<')) - def register_adapt_intent(self, name: str, intent_parser: object): - """ - Register an Adapt intent parser object. Serializes the intent_parser - and sends it over the messagebus to registered. - @param name: string intent name (without skill_id prefix) - @param intent_parser: Adapt Intent object - """ - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward("register_intent", intent_parser.__dict__)) - self.registered_intents.append((name, intent_parser)) - self.detached_intents = [detached for detached in self.detached_intents - if detached[0] != name] - - def detach_intent(self, intent_name: str): - """ - DEPRECATED: Use `remove_intent` instead, all other methods from this - class expect intent_name; this was the weird one expecting the internal - munged intent_name with skill_id. - """ - name = intent_name.split(':')[1] - log_deprecation(f"Update to `self.remove_intent({name})", - "0.1.0") - self.remove_intent(name) - def remove_intent(self, intent_name: str): - """ - Remove an intent from the intent service. The intent is saved in the - list of detached intents for use when re-enabling an intent. A - `detach_intent` Message is emitted for the intent service to handle. - @param intent_name: Registered intent to remove/detach (no skill_id) - """ - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - if intent_name in self.intent_names: - # TODO: This will create duplicates of already detached intents - LOG.info(f"Detaching intent: {intent_name}") - self.detached_intents.append((intent_name, - self.get_intent(intent_name))) - self.registered_intents = [pair for pair in self.registered_intents - if pair[0] != intent_name] - self.bus.emit(msg.forward("detach_intent", - {"intent_name": - f"{self.skill_id}:{intent_name}"})) - - def intent_is_detached(self, intent_name: str) -> bool: + def munge_intent_parser(intent_parser, name, skill_id): """ - Determine if an intent is detached. - @param intent_name: String intent reference to check (without skill_id) - @return: True if intent is in detached_intents, else False. + Rename intent keywords to make them skill exclusive + This gives the intent parser an exclusive name in the + format :. The keywords are given unique + names in the format . + + The function will not munge instances that's already been + munged + + Args: + intent_parser: (IntentParser) object to update + name: (str) Skill name + skill_id: (int) skill identifier """ - is_detached = False - with self._iterator_lock: - for (name, _) in self.detached_intents: - if name == intent_name: - is_detached = True - break - return is_detached - - def set_adapt_context(self, context: str, word: str, origin: str): - """ - Set an Adapt context. - @param context: context keyword name to add/update - @param word: word to register (context keyword value) - @param origin: original origin of the context (for cross context) - """ - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward('add_context', - {'context': context, 'word': word, - 'origin': origin})) - - def remove_adapt_context(self, context: str): - """ - Remove an Adapt context. - @param context: context keyword name to remove + # Munge parser name + if not name.startswith(str(skill_id) + ':'): + intent_parser.name = str(skill_id) + ':' + name + else: + intent_parser.name = name + + # Munge keywords + skill_id = to_alnum(skill_id) + # Munge required keyword + reqs = [] + for i in intent_parser.requires: + if not i[0].startswith(skill_id): + kw = (skill_id + i[0], skill_id + i[0]) + reqs.append(kw) + else: + reqs.append(i) + intent_parser.requires = reqs + + # Munge optional keywords + opts = [] + for i in intent_parser.optional: + if not i[0].startswith(skill_id): + kw = (skill_id + i[0], skill_id + i[0]) + opts.append(kw) + else: + opts.append(i) + intent_parser.optional = opts + + # Munge at_least_one keywords + at_least_one = [] + for i in intent_parser.at_least_one: + element = [skill_id + e.replace(skill_id, '') for e in i] + at_least_one.append(tuple(element)) + intent_parser.at_least_one = at_least_one + + + class IntentServiceInterface: """ - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward('remove_context', {'context': context})) + Interface to communicate with the Mycroft intent service. - def register_padatious_intent(self, intent_name: str, filename: str, - lang: str): + This class wraps the messagebus interface of the intent service allowing + for easier interaction with the service. It wraps both the Adapt and + Padatious parts of the intent services. """ - Register a Padatious intent file with the intent service. - @param intent_name: Unique intent identifier - (usually `skill_id`:`filename`) - @param filename: Absolute file path to entity file - @param lang: BCP-47 language code of registered intent - """ - if not isinstance(filename, str): - raise ValueError('Filename path must be a string') - if not exists(filename): - raise FileNotFoundError(f'Unable to find "{filename}"') - with open(filename) as f: - samples = [_ for _ in f.read().split("\n") if _ - and not _.startswith("#")] - data = {'file_name': filename, - "samples": samples, - 'name': intent_name, - 'lang': lang} - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward("padatious:register_intent", data)) - self.registered_intents.append((intent_name.split(':')[-1], data)) - - def register_padatious_entity(self, entity_name: str, filename: str, - lang: str): - """ - Register a Padatious entity file with the intent service. - @param entity_name: Unique entity identifier - (usually `skill_id`:`filename`) - @param filename: Absolute file path to entity file - @param lang: BCP-47 language code of registered intent - """ - if not isinstance(filename, str): - raise ValueError('Filename path must be a string') - if not exists(filename): - raise FileNotFoundError('Unable to find "{}"'.format(filename)) - with open(filename) as f: - samples = [_ for _ in f.read().split("\n") if _ - and not _.startswith("#")] - msg = dig_for_message() or Message("") - if "skill_id" not in msg.context: - msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward('padatious:register_entity', - {'file_name': filename, - "samples": samples, - 'name': entity_name, - 'lang': lang})) - - def get_intent_names(self): - log_deprecation("Reference `intent_names` directly", "0.1.0") - return self.intent_names - - def detach_all(self): - """ - Detach all intents associated with this interface and remove all - internal references to intents and handlers. - """ - for name in self.intent_names: + + def __init__(self, bus=None): + self._bus = bus + self.skill_id = self.__class__.__name__ + # TODO: Consider using properties with setters to prevent duplicates + self.registered_intents: List[Tuple[str, object]] = [] + self.detached_intents: List[Tuple[str, object]] = [] + self._iterator_lock = RLock() + + @property + def intent_names(self) -> List[str]: + """ + Get a list of intent names (both registered and disabled). + """ + return [a[0] for a in self.registered_intents + self.detached_intents] + + @property + def bus(self): + if not self._bus: + raise RuntimeError("bus not set. call `set_bus()` before trying to" + "interact with the Messagebus") + return self._bus + + @bus.setter + def bus(self, val): + self.set_bus(val) + + def set_bus(self, bus=None): + self._bus = bus or ovos_utils.messagebus.get_mycroft_bus() + + def set_id(self, skill_id: str): + self.skill_id = skill_id + + def register_adapt_keyword(self, vocab_type: str, entity: str, + aliases: Optional[List[str]] = None, + lang: str = None): + """ + Send a message to the intent service to add an Adapt keyword. + @param vocab_type: Keyword reference (file basename) + @param entity: Primary keyword value + @param aliases: List of alternative keyword values + @param lang: BCP-47 language code of entity and aliases + """ + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + + # TODO 22.02: Remove compatibility data + aliases = aliases or [] + entity_data = {'entity_value': entity, + 'entity_type': vocab_type, + 'lang': lang} + compatibility_data = {'start': entity, 'end': vocab_type} + + self.bus.emit(msg.forward("register_vocab", + {**entity_data, **compatibility_data})) + for alias in aliases: + alias_data = { + 'entity_value': alias, + 'entity_type': vocab_type, + 'alias_of': entity, + 'lang': lang} + compatibility_data = {'start': alias, 'end': vocab_type} + self.bus.emit(msg.forward("register_vocab", + {**alias_data, **compatibility_data})) + + def register_adapt_regex(self, regex: str, lang: str = None): + """ + Register a regex string with the intent service. + @param regex: Regex to be registered; Adapt extracts keyword references + from named match group. + @param lang: BCP-47 language code of regex + """ + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward("register_vocab", + {'regex': regex, 'lang': lang})) + + def register_adapt_intent(self, name: str, intent_parser: object): + """ + Register an Adapt intent parser object. Serializes the intent_parser + and sends it over the messagebus to registered. + @param name: string intent name (without skill_id prefix) + @param intent_parser: Adapt Intent object + """ + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward("register_intent", intent_parser.__dict__)) + self.registered_intents.append((name, intent_parser)) + self.detached_intents = [detached for detached in self.detached_intents + if detached[0] != name] + + def detach_intent(self, intent_name: str): + """ + DEPRECATED: Use `remove_intent` instead, all other methods from this + class expect intent_name; this was the weird one expecting the internal + munged intent_name with skill_id. + """ + name = intent_name.split(':')[1] + log_deprecation(f"Update to `self.remove_intent({name})", + "0.1.0") self.remove_intent(name) - if self.registered_intents: - LOG.error(f"Expected an empty list; got: {self.registered_intents}") - self.registered_intents = [] - self.detached_intents = [] # Explicitly remove all intent references - def get_intent(self, intent_name: str) -> Optional[object]: - """ - Get an intent object by name. This will find both enabled and disabled - intents. - @param intent_name: name of intent to find (without skill_id) - @return: intent object if found, else None - """ - to_return = None - with self._iterator_lock: - for name, intent in self.registered_intents: - if name == intent_name: - to_return = intent - break - if to_return is None: + def remove_intent(self, intent_name: str): + """ + Remove an intent from the intent service. The intent is saved in the + list of detached intents for use when re-enabling an intent. A + `detach_intent` Message is emitted for the intent service to handle. + @param intent_name: Registered intent to remove/detach (no skill_id) + """ + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + if intent_name in self.intent_names: + # TODO: This will create duplicates of already detached intents + LOG.info(f"Detaching intent: {intent_name}") + self.detached_intents.append((intent_name, + self.get_intent(intent_name))) + self.registered_intents = [pair for pair in self.registered_intents + if pair[0] != intent_name] + self.bus.emit(msg.forward("detach_intent", + {"intent_name": + f"{self.skill_id}:{intent_name}"})) + + def intent_is_detached(self, intent_name: str) -> bool: + """ + Determine if an intent is detached. + @param intent_name: String intent reference to check (without skill_id) + @return: True if intent is in detached_intents, else False. + """ + is_detached = False + with self._iterator_lock: + for (name, _) in self.detached_intents: + if name == intent_name: + is_detached = True + break + return is_detached + + def set_adapt_context(self, context: str, word: str, origin: str): + """ + Set an Adapt context. + @param context: context keyword name to add/update + @param word: word to register (context keyword value) + @param origin: original origin of the context (for cross context) + """ + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward('add_context', + {'context': context, 'word': word, + 'origin': origin})) + + def remove_adapt_context(self, context: str): + """ + Remove an Adapt context. + @param context: context keyword name to remove + """ + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward('remove_context', {'context': context})) + + def register_padatious_intent(self, intent_name: str, filename: str, + lang: str): + """ + Register a Padatious intent file with the intent service. + @param intent_name: Unique intent identifier + (usually `skill_id`:`filename`) + @param filename: Absolute file path to entity file + @param lang: BCP-47 language code of registered intent + """ + if not isinstance(filename, str): + raise ValueError('Filename path must be a string') + if not exists(filename): + raise FileNotFoundError(f'Unable to find "{filename}"') + with open(filename) as f: + samples = [_ for _ in f.read().split("\n") if _ + and not _.startswith("#")] + data = {'file_name': filename, + "samples": samples, + 'name': intent_name, + 'lang': lang} + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward("padatious:register_intent", data)) + self.registered_intents.append((intent_name.split(':')[-1], data)) + + def register_padatious_entity(self, entity_name: str, filename: str, + lang: str): + """ + Register a Padatious entity file with the intent service. + @param entity_name: Unique entity identifier + (usually `skill_id`:`filename`) + @param filename: Absolute file path to entity file + @param lang: BCP-47 language code of registered intent + """ + if not isinstance(filename, str): + raise ValueError('Filename path must be a string') + if not exists(filename): + raise FileNotFoundError('Unable to find "{}"'.format(filename)) + with open(filename) as f: + samples = [_ for _ in f.read().split("\n") if _ + and not _.startswith("#")] + msg = ovos_utils.messagebus.dig_for_message() or ovos_utils.messagebus.Message("") + if "skill_id" not in msg.context: + msg.context["skill_id"] = self.skill_id + self.bus.emit(msg.forward('padatious:register_entity', + {'file_name': filename, + "samples": samples, + 'name': entity_name, + 'lang': lang})) + + def get_intent_names(self): + log_deprecation("Reference `intent_names` directly", "0.1.0") + return self.intent_names + + def detach_all(self): + """ + Detach all intents associated with this interface and remove all + internal references to intents and handlers. + """ + for name in self.intent_names: + self.remove_intent(name) + if self.registered_intents: + LOG.error(f"Expected an empty list; got: {self.registered_intents}") + self.registered_intents = [] + self.detached_intents = [] # Explicitly remove all intent references + + def get_intent(self, intent_name: str) -> Optional[object]: + """ + Get an intent object by name. This will find both enabled and disabled + intents. + @param intent_name: name of intent to find (without skill_id) + @return: intent object if found, else None + """ + to_return = None with self._iterator_lock: - for name, intent in self.detached_intents: + for name, intent in self.registered_intents: if name == intent_name: to_return = intent break - return to_return - - def __iter__(self): - """Iterator over the registered intents. - - Returns an iterator returning name-handler pairs of the registered - intent handlers. - """ - return iter(self.registered_intents) - - def __contains__(self, val): + if to_return is None: + with self._iterator_lock: + for name, intent in self.detached_intents: + if name == intent_name: + to_return = intent + break + return to_return + + def __iter__(self): + """Iterator over the registered intents. + + Returns an iterator returning name-handler pairs of the registered + intent handlers. + """ + return iter(self.registered_intents) + + def __contains__(self, val): + """ + Checks if an intent name has been registered. + """ + return val in [i[0] for i in self.registered_intents] + + + def open_intent_envelope(message): """ - Checks if an intent name has been registered. + Convert dictionary received over messagebus to Intent. """ - return val in [i[0] for i in self.registered_intents] + # TODO can this method be fully removed from ovos_utils ? + from adapt.intent import Intent + + intent_dict = message.data + return Intent(intent_dict.get('name'), + intent_dict.get('requires'), + intent_dict.get('at_least_one'), + intent_dict.get('optional')) class IntentQueryApi: @@ -365,14 +384,15 @@ class IntentQueryApi: """ def __init__(self, bus=None, timeout=5): + LOG.warning("IntentQueryApi has been deprecated and will be removed in 0.1.0") if bus is None: - bus = get_mycroft_bus() + bus = ovos_utils.messagebus.get_mycroft_bus() self.bus = bus self.timeout = timeout def get_adapt_intent(self, utterance, lang="en-us"): """ get best adapt intent for utterance """ - msg = Message("intent.service.adapt.get", + msg = ovos_utils.messagebus.Message("intent.service.adapt.get", {"utterance": utterance, "lang": lang}, context={"destination": "intent_service", "source": "intent_api"}) @@ -388,7 +408,7 @@ def get_adapt_intent(self, utterance, lang="en-us"): def get_padatious_intent(self, utterance, lang="en-us"): """ get best padatious intent for utterance """ - msg = Message("intent.service.padatious.get", + msg = ovos_utils.messagebus.Message("intent.service.padatious.get", {"utterance": utterance, "lang": lang}, context={"destination": "intent_service", "source": "intent_api"}) @@ -403,7 +423,7 @@ def get_padatious_intent(self, utterance, lang="en-us"): def get_intent(self, utterance, lang="en-us"): """ get best intent for utterance """ - msg = Message("intent.service.intent.get", + msg = ovos_utils.messagebus.Message("intent.service.intent.get", {"utterance": utterance, "lang": lang}, context={"destination": "intent_service", "source": "intent_api"}) @@ -432,7 +452,7 @@ def get_skill(self, utterance, lang="en-us"): return None # raise some error here maybe? this should never happen def get_skills_manifest(self): - msg = Message("intent.service.skills.get", + msg = ovos_utils.messagebus.Message("intent.service.skills.get", context={"destination": "intent_service", "source": "intent_api"}) resp = self.bus.wait_for_response(msg, @@ -445,7 +465,7 @@ def get_skills_manifest(self): return data["skills"] def get_active_skills(self, include_timestamps=False): - msg = Message("intent.service.active_skills.get", + msg = ovos_utils.messagebus.Message("intent.service.active_skills.get", context={"destination": "intent_service", "source": "intent_api"}) resp = self.bus.wait_for_response(msg, @@ -460,7 +480,7 @@ def get_active_skills(self, include_timestamps=False): return [s[0] for s in data["skills"]] def get_adapt_manifest(self): - msg = Message("intent.service.adapt.manifest.get", + msg = ovos_utils.messagebus.Message("intent.service.adapt.manifest.get", context={"destination": "intent_service", "source": "intent_api"}) resp = self.bus.wait_for_response(msg, @@ -473,7 +493,7 @@ def get_adapt_manifest(self): return data["intents"] def get_padatious_manifest(self): - msg = Message("intent.service.padatious.manifest.get", + msg = ovos_utils.messagebus.Message("intent.service.padatious.manifest.get", context={"destination": "intent_service", "source": "intent_api"}) resp = self.bus.wait_for_response(msg, @@ -492,7 +512,7 @@ def get_intent_manifest(self): "padatious": padatious} def get_vocab_manifest(self): - msg = Message("intent.service.adapt.vocab.manifest.get", + msg = ovos_utils.messagebus.Message("intent.service.adapt.vocab.manifest.get", context={"destination": "intent_service", "source": "intent_api"}) reply_msg_type = 'intent.service.adapt.vocab.manifest' @@ -515,7 +535,7 @@ def get_vocab_manifest(self): for voc in vocab] def get_regex_manifest(self): - msg = Message("intent.service.adapt.vocab.manifest.get", + msg = ovos_utils.messagebus.Message("intent.service.adapt.vocab.manifest.get", context={"destination": "intent_service", "source": "intent_api"}) reply_msg_type = 'intent.service.adapt.vocab.manifest' @@ -539,7 +559,7 @@ def get_regex_manifest(self): for voc in vocab] def get_entities_manifest(self): - msg = Message("intent.service.padatious.entities.manifest.get", + msg = ovos_utils.messagebus.Message("intent.service.padatious.entities.manifest.get", context={"destination": "intent_service", "source": "intent_api"}) reply_msg_type = 'intent.service.padatious.entities.manifest' @@ -573,15 +593,3 @@ def get_keywords_manifest(self): "regex": regex} -def open_intent_envelope(message): - """ - Convert dictionary received over messagebus to Intent. - """ - # TODO can this method be fully removed from ovos_utils ? - from adapt.intent import Intent - - intent_dict = message.data - return Intent(intent_dict.get('name'), - intent_dict.get('requires'), - intent_dict.get('at_least_one'), - intent_dict.get('optional')) diff --git a/ovos_utils/intents/layers.py b/ovos_utils/intents/layers.py index 7e7bfb76..a699bf62 100644 --- a/ovos_utils/intents/layers.py +++ b/ovos_utils/intents/layers.py @@ -1,156 +1,162 @@ -from ovos_utils.messagebus import get_mycroft_bus, FakeMessage as Message from ovos_utils.log import LOG -from time import sleep - -class IntentLayers: - def __init__(self, bus=None, layers=None): - # TODO: Deprecate in 0.1.0 - LOG.error(f"This module is deprecated, import from `ovos_workshop.skills.layers") - layers = layers or [] - self.bus = bus or get_mycroft_bus() - # make intent levels for N layers - self.layers = layers - self.current_layer = 0 - self.activate_layer(0) - self.named_layers = {} - - def disable_intent(self, intent_name): - """Disable a registered intent""" - self.bus.emit(Message("mycroft.skill.disable_intent", - {"intent_name": intent_name})) - - def enable_intent(self, intent_name): - """Reenable a registered self intent""" - self.bus.emit(Message("mycroft.skill.enable_intent", - {"intent_name": intent_name})) - - def reset(self): - LOG.info("Reseting Intent Layers") - self.activate_layer(0) - - def next(self): - LOG.info("Going to next Intent Layer") - self.current_layer += 1 - if self.current_layer > len(self.layers): - LOG.info("Already in last layer, going to layer 0") - self.current_layer = 0 - self.activate_layer(self.current_layer) - - def previous(self): - LOG.info("Going to previous Intent Layer") - self.current_layer -= 1 - if self.current_layer < 0: +LOG.warning("IntentLayers moved to ovos_workshop.decorators.layers") + +try: + from ovos_workshop.decorators.layers import IntentLayers +except ImportError: + import ovos_utils.messagebus + + from time import sleep + + class IntentLayers: + def __init__(self, bus=None, layers=None): + # TODO: Deprecate in 0.1.0 + LOG.error(f"This module is deprecated, import from `ovos_workshop.skills.layers") + layers = layers or [] + self.bus = bus or ovos_utils.messagebus.get_mycroft_bus() + # make intent levels for N layers + self.layers = layers self.current_layer = 0 - LOG.error("Already in layer 0") - else: + self.activate_layer(0) + self.named_layers = {} + + def disable_intent(self, intent_name): + """Disable a registered intent""" + self.bus.emit(ovos_utils.messagebus.Message("mycroft.skill.disable_intent", + {"intent_name": intent_name})) + + def enable_intent(self, intent_name): + """Reenable a registered self intent""" + self.bus.emit(ovos_utils.messagebus.Message("mycroft.skill.enable_intent", + {"intent_name": intent_name})) + + def reset(self): + LOG.info("Reseting Intent Layers") + self.activate_layer(0) + + def next(self): + LOG.info("Going to next Intent Layer") + self.current_layer += 1 + if self.current_layer > len(self.layers): + LOG.info("Already in last layer, going to layer 0") + self.current_layer = 0 self.activate_layer(self.current_layer) - - def add_layer(self, intent_list=None): - intent_list = intent_list or [] - self.layers.append(intent_list) - LOG.info("Adding intent layer: " + str(intent_list)) - - def add_named_layer(self, name, intent_list=None): - intent_list = intent_list or [] - self.named_layers[name] = len(self.layers) - self.layers.append(intent_list) - LOG.info("Setting layer " + name + " to: " + str(intent_list)) - - def activate_named_layer(self, name): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("activating layer named: " + name) - self.activate_layer(i) - else: - LOG.error("no layer named: " + name) - - def deactivate_named_layer(self, name): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("deactivating layer named: " + name) - self.deactivate_layer(i) - else: - LOG.error("no layer named: " + name) - - def remove_named_layer(self, name): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("removing layer named: " + name) - self.remove_layer(i) - else: - LOG.error("no layer named: " + name) - - def replace_named_layer(self, name, intent_list=None): - if name in self.named_layers: - i = self.named_layers[name] - LOG.info("replacing layer named: " + name) - self.replace_layer(i, intent_list) - else: - LOG.error("no layer named: " + name) - - def replace_layer(self, layer_num, intent_list=None): - intent_list = intent_list or [] - if self.current_layer == layer_num: - self.deactivate_layer(layer_num) - LOG.info("Adding layer" + str(intent_list) + " in position " + str( - layer_num)) - self.layers[layer_num] = intent_list - if self.current_layer == layer_num: - self.activate_layer(layer_num) - - def remove_layer(self, layer_num): - if layer_num >= len(self.layers): - return False - if self.current_layer == layer_num: - self.deactivate_layer(layer_num) - self.layers.pop(layer_num) - LOG.info("Removing layer number " + str(layer_num)) - return True - - def find_layer(self, intent_name): - layer_list = [] - for i in range(0, len(self.layers)): - if intent_name in self.layers[i]: - layer_list.append(i) - return layer_list - - def disable(self): - LOG.info("Disabling layers") - # disable all layers - for i in range(0, len(self.layers)): - self.deactivate_layer(i) - - def activate_layer(self, layer_num): - # error check - if layer_num < 0 or layer_num > len(self.layers): - LOG.error("invalid layer number") + + def previous(self): + LOG.info("Going to previous Intent Layer") + self.current_layer -= 1 + if self.current_layer < 0: + self.current_layer = 0 + LOG.error("Already in layer 0") + else: + self.activate_layer(self.current_layer) + + def add_layer(self, intent_list=None): + intent_list = intent_list or [] + self.layers.append(intent_list) + LOG.info("Adding intent layer: " + str(intent_list)) + + def add_named_layer(self, name, intent_list=None): + intent_list = intent_list or [] + self.named_layers[name] = len(self.layers) + self.layers.append(intent_list) + LOG.info("Setting layer " + name + " to: " + str(intent_list)) + + def activate_named_layer(self, name): + if name in self.named_layers: + i = self.named_layers[name] + LOG.info("activating layer named: " + name) + self.activate_layer(i) + else: + LOG.error("no layer named: " + name) + + def deactivate_named_layer(self, name): + if name in self.named_layers: + i = self.named_layers[name] + LOG.info("deactivating layer named: " + name) + self.deactivate_layer(i) + else: + LOG.error("no layer named: " + name) + + def remove_named_layer(self, name): + if name in self.named_layers: + i = self.named_layers[name] + LOG.info("removing layer named: " + name) + self.remove_layer(i) + else: + LOG.error("no layer named: " + name) + + def replace_named_layer(self, name, intent_list=None): + if name in self.named_layers: + i = self.named_layers[name] + LOG.info("replacing layer named: " + name) + self.replace_layer(i, intent_list) + else: + LOG.error("no layer named: " + name) + + def replace_layer(self, layer_num, intent_list=None): + intent_list = intent_list or [] + if self.current_layer == layer_num: + self.deactivate_layer(layer_num) + LOG.info("Adding layer" + str(intent_list) + " in position " + str( + layer_num)) + self.layers[layer_num] = intent_list + if self.current_layer == layer_num: + self.activate_layer(layer_num) + + def remove_layer(self, layer_num): + if layer_num >= len(self.layers): + return False + if self.current_layer == layer_num: + self.deactivate_layer(layer_num) + self.layers.pop(layer_num) + LOG.info("Removing layer number " + str(layer_num)) + return True + + def find_layer(self, intent_name): + layer_list = [] + for i in range(0, len(self.layers)): + if intent_name in self.layers[i]: + layer_list.append(i) + return layer_list + + def disable(self): + LOG.info("Disabling layers") + # disable all layers + for i in range(0, len(self.layers)): + self.deactivate_layer(i) + + def activate_layer(self, layer_num): + # error check + if layer_num < 0 or layer_num > len(self.layers): + LOG.error("invalid layer number") + return False + + self.current_layer = layer_num + + # disable other layers + self.disable() + + # TODO in here we should wait for all intents to be detached + # sometimes detach intent from this step comes after register from next + # is there some bus signal we can track? + sleep(0.3) + + # enable layer + LOG.info("Activating Layer " + str(layer_num)) + if layer_num < len(self.layers) and len(self.layers): + for intent_name in self.layers[layer_num]: + self.enable_intent(intent_name) + return True return False - - self.current_layer = layer_num - - # disable other layers - self.disable() - - # TODO in here we should wait for all intents to be detached - # sometimes detach intent from this step comes after register from next - # is there some bus signal we can track? - sleep(0.3) - - # enable layer - LOG.info("Activating Layer " + str(layer_num)) - if layer_num < len(self.layers) and len(self.layers): + + def deactivate_layer(self, layer_num): + # error check + if layer_num < 0 or layer_num > len(self.layers): + LOG.error("invalid layer number") + return False + LOG.info("Deactivating Layer " + str(layer_num)) for intent_name in self.layers[layer_num]: - self.enable_intent(intent_name) + self.disable_intent(intent_name) return True - return False - - def deactivate_layer(self, layer_num): - # error check - if layer_num < 0 or layer_num > len(self.layers): - LOG.error("invalid layer number") - return False - LOG.info("Deactivating Layer " + str(layer_num)) - for intent_name in self.layers[layer_num]: - self.disable_intent(intent_name) - return True diff --git a/ovos_utils/messagebus.py b/ovos_utils/messagebus.py index d62f97a4..6bea298e 100644 --- a/ovos_utils/messagebus.py +++ b/ovos_utils/messagebus.py @@ -11,6 +11,8 @@ from ovos_utils.json_helper import merge_dict from ovos_utils.log import LOG, log_deprecation from ovos_utils.metrics import Stopwatch +from ovos_utils.events import EventContainer, get_handler_name, create_basic_wrapper, create_wrapper, unmunge_message + _DEFAULT_WS_CONFIG = {"host": "0.0.0.0", "port": 8181, @@ -541,190 +543,6 @@ def to_alnum(skill_id): return ''.join(c if c.isalnum() else '_' for c in str(skill_id)) -def unmunge_message(message, skill_id): - """Restore message keywords by removing the Letterified skill ID. - Args: - message (FakeMessage): Intent result message - skill_id (str): skill identifier - Returns: - Message without clear keywords - """ - if hasattr(message, "data") and isinstance(message.data, dict): - skill_id = to_alnum(skill_id) - for key in list(message.data.keys()): - if key.startswith(skill_id): - # replace the munged key with the real one - new_key = key[len(skill_id):] - message.data[new_key] = message.data.pop(key) - - return message - - -def get_handler_name(handler): - """Name (including class if available) of handler function. - - Args: - handler (function): Function to be named - - Returns: - string: handler name as string - """ - if '__self__' in dir(handler) and 'name' in dir(handler.__self__): - return handler.__self__.name + '.' + handler.__name__ - else: - return handler.__name__ - - -def create_wrapper(handler, skill_id, on_start, on_end, on_error): - """Create the default skill handler wrapper. - - This wrapper handles things like metrics, reporting handler start/stop - and errors. - handler (callable): method/function to call - skill_id: skill_id for associated skill - on_start (function): function to call before executing the handler - on_end (function): function to call after executing the handler - on_error (function): function to call for error reporting - """ - - def wrapper(message): - stopwatch = Stopwatch() - try: - message = unmunge_message(message, skill_id) - if on_start: - on_start(message) - - with stopwatch: - if len(signature(handler).parameters) == 0: - handler() - else: - handler(message) - - except Exception as e: - if on_error: - if len(signature(on_error).parameters) == 2: - on_error(e, message) - else: - on_error(e) - finally: - if on_end: - on_end(message) - - # Send timing metrics - context = message.context - if context and 'ident' in context: - try: - from mycroft.metrics import report_timing - report_timing(context['ident'], 'skill_handler', stopwatch, - {'handler': handler.__name__, - 'skill_id': skill_id}) - except ImportError: - pass - - return wrapper - - -def create_basic_wrapper(handler, on_error=None): - """Create the default skill handler wrapper. - - This wrapper handles things like metrics, reporting handler start/stop - and errors. - - Args: - handler (callable): method/function to call - on_error (function): function to call to report error. - - Returns: - Wrapped callable - """ - - def wrapper(message): - try: - if len(signature(handler).parameters) == 0: - handler() - else: - handler(message) - except Exception as e: - if on_error: - on_error(e) - - return wrapper - - -class EventContainer: - def __init__(self, bus=None): - log_deprecation("Import `ovos_utils.events.EventContainer", "0.1.0") - self.bus = bus - self.events = [] - - def set_bus(self, bus): - self.bus = bus - - def add(self, name, handler, once=False): - """Create event handler for executing intent or other event. - Args: - name (string): IntentParser name - handler (func): Method to call - once (bool, optional): Event handler will be removed after it has - been run once. - """ - - def once_wrapper(message): - # Remove registered one-time handler before invoking, - # allowing them to re-schedule themselves. - self.remove(name) - handler(message) - - if handler: - if once: - self.bus.once(name, once_wrapper) - self.events.append((name, once_wrapper)) - else: - self.bus.on(name, handler) - self.events.append((name, handler)) - - LOG.debug('Added event: {}'.format(name)) - - def remove(self, name): - """Removes an event from bus emitter and events list. - Args: - name (string): Name of Intent or Scheduler Event - Returns: - bool: True if found and removed, False if not found - """ - LOG.debug("Removing event {}".format(name)) - removed = False - for _name, _handler in list(self.events): - if name == _name: - try: - self.events.remove((_name, _handler)) - except ValueError: - LOG.error('Failed to remove event {}'.format(name)) - pass - removed = True - - # Because of function wrappers, the emitter doesn't always directly - # hold the _handler function, it sometimes holds something like - # 'wrapper(_handler)'. So a call like: - # self.bus.remove(_name, _handler) - # will not find it, leaving an event handler with that name left behind - # waiting to fire if it is ever re-installed and triggered. - # Remove all handlers with the given name, regardless of handler. - if removed: - self.bus.remove_all_listeners(name) - return removed - - def __iter__(self): - return iter(self.events) - - def clear(self): - """Unregister all registered handlers and clear the list of registered - events. - """ - for e, f in self.events: - self.bus.remove(e, f) - self.events = [] # Remove reference to wrappers - class BusService: """ Provide some service over the messagebus for other components diff --git a/ovos_utils/skills/api.py b/ovos_utils/skills/api.py index 438a125b..f85b0908 100644 --- a/ovos_utils/skills/api.py +++ b/ovos_utils/skills/api.py @@ -1,82 +1,74 @@ -# Copyright 2020 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Dict, Optional -from ovos_bus_client.message import Message from ovos_utils.log import LOG +LOG.warning("ovos_utils.skills.api moved to ovos_workshop.skills.api") -class SkillApi: - """ - SkillApi provides a MessageBus interface to specific registered methods. - Methods decorated with `@skill_api_method` are exposed via the messagebus. - To use a skill's API methods, call `SkillApi.get` with the requested skill's - ID and an object is returned with an interface to all exposed methods. - """ - bus = None +try: + from ovos_workshop.skills.api import SkillApi +except: + from typing import Dict, Optional + from ovos_utils.messagebus import Message - @classmethod - def connect_bus(cls, mycroft_bus): - """Registers the bus object to use.""" - cls.bus = mycroft_bus - def __init__(self, method_dict: Dict[str, dict], timeout: int = 3): + class SkillApi: """ - Initialize a SkillApi for the given methods - @param method_dict: dict of method name to dict containing: - `help` - method docstring - `type` - string Message type associated with this method - @param timeout: Seconds to wait for a Skill API response + SkillApi provides a MessageBus interface to specific registered methods. + Methods decorated with `@skill_api_method` are exposed via the messagebus. + To use a skill's API methods, call `SkillApi.get` with the requested skill's + ID and an object is returned with an interface to all exposed methods. """ - self.method_dict = method_dict - self.timeout = timeout - for key in method_dict: - def get_method(k): - def method(*args, **kwargs): - m = self.method_dict[k] - data = {'args': args, 'kwargs': kwargs} - method_msg = Message(m['type'], data) - response = \ - SkillApi.bus.wait_for_response(method_msg, - timeout=self.timeout) - if not response: - LOG.error(f"Timed out waiting for {method_msg}") - return None - elif 'result' not in response.data: - LOG.error(f"missing `result` in: {response.data}") - else: - return response.data['result'] + bus = None - return method + @classmethod + def connect_bus(cls, mycroft_bus): + """Registers the bus object to use.""" + cls.bus = mycroft_bus - self.__setattr__(key, get_method(key)) + def __init__(self, method_dict: Dict[str, dict], timeout: int = 3): + """ + Initialize a SkillApi for the given methods + @param method_dict: dict of method name to dict containing: + `help` - method docstring + `type` - string Message type associated with this method + @param timeout: Seconds to wait for a Skill API response + """ + self.method_dict = method_dict + self.timeout = timeout + for key in method_dict: + def get_method(k): + def method(*args, **kwargs): + m = self.method_dict[k] + data = {'args': args, 'kwargs': kwargs} + method_msg = Message(m['type'], data) + response = \ + SkillApi.bus.wait_for_response(method_msg, + timeout=self.timeout) + if not response: + LOG.error(f"Timed out waiting for {method_msg}") + return None + elif 'result' not in response.data: + LOG.error(f"missing `result` in: {response.data}") + else: + return response.data['result'] - @staticmethod - def get(skill: str, api_timeout: int = 3) -> Optional[object]: - """ - Generate a SkillApi object for the requested skill if that skill exposes - and API methods. - @param skill: ID of skill to get an API object for - @param api_timeout: seconds to wait for a skill API response - @return: SkillApi object if available, else None - """ - if not SkillApi.bus: - raise RuntimeError("Requested update before `SkillAPI.bus` is set. " - "Call `SkillAPI.connect_bus` first.") - public_api_msg = f'{skill}.public_api' - api = SkillApi.bus.wait_for_response(Message(public_api_msg)) - if api: - return SkillApi(api.data, api_timeout) - else: - return None + return method + + self.__setattr__(key, get_method(key)) + + @staticmethod + def get(skill: str, api_timeout: int = 3) -> Optional[object]: + """ + Generate a SkillApi object for the requested skill if that skill exposes + and API methods. + @param skill: ID of skill to get an API object for + @param api_timeout: seconds to wait for a skill API response + @return: SkillApi object if available, else None + """ + if not SkillApi.bus: + raise RuntimeError("Requested update before `SkillAPI.bus` is set. " + "Call `SkillAPI.connect_bus` first.") + public_api_msg = f'{skill}.public_api' + api = SkillApi.bus.wait_for_response(Message(public_api_msg)) + if api: + return SkillApi(api.data, api_timeout) + else: + return None diff --git a/ovos_utils/skills/audioservice.py b/ovos_utils/skills/audioservice.py index 26ed3559..8c01c940 100644 --- a/ovos_utils/skills/audioservice.py +++ b/ovos_utils/skills/audioservice.py @@ -12,209 +12,274 @@ # See the License for the specific language governing permissions and # limitations under the License. # - from datetime import timedelta -# This file is directly copied from HolmesV, it's a simple utility to -# interface with the AudioService via messagebus outside core -from os.path import abspath from ovos_utils.log import LOG, deprecated -from ovos_utils.messagebus import get_mycroft_bus, dig_for_message, \ - FakeMessage as Message - - -def ensure_uri(s: str): - """ - Interpret paths as file:// uri's. - - Args: - s: string path to be checked - - Returns: - if s is uri, s is returned otherwise file:// is prepended - """ - if isinstance(s, str): - if '://' not in s: - return 'file://' + abspath(s) - else: - return s - elif isinstance(s, (tuple, list)): # Handle (mime, uri) arg - if '://' not in s[0]: - return 'file://' + abspath(s[0]), s[1] - else: - return s - else: - raise ValueError('Invalid track') - - -class ClassicAudioServiceInterface: - """AudioService class for interacting with the audio subsystem - - Audio is managed by OCP in the default implementation, - usually this class should not be directly used, see OCPInterface instead - - Args: - bus: Mycroft messagebus connection - """ - - def __init__(self, bus=None): - self.bus = bus or get_mycroft_bus() - - def queue(self, tracks=None): - """Queue up a track to playing playlist. - - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - """ - tracks = tracks or [] - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [ensure_uri(t) for t in tracks] - self.bus.emit(Message('mycroft.audio.service.queue', - data={'tracks': tracks})) - - def play(self, tracks=None, utterance=None, repeat=None): - """Start playback. - - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - utterance: forward utterance for further processing by the - audio service. - repeat: if the playback should be looped - """ - repeat = repeat or False - tracks = tracks or [] - utterance = utterance or '' - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [ensure_uri(t) for t in tracks] - self.bus.emit(Message('mycroft.audio.service.play', - data={'tracks': tracks, - 'utterance': utterance, - 'repeat': repeat})) - - def stop(self): - """Stop the track.""" - self.bus.emit(Message('mycroft.audio.service.stop')) - - def next(self): - """Change to next track.""" - self.bus.emit(Message('mycroft.audio.service.next')) - - def prev(self): - """Change to previous track.""" - self.bus.emit(Message('mycroft.audio.service.prev')) - - def pause(self): - """Pause playback.""" - self.bus.emit(Message('mycroft.audio.service.pause')) - - def resume(self): - """Resume paused playback.""" - self.bus.emit(Message('mycroft.audio.service.resume')) - - def get_track_length(self): - """ - getting the duration of the audio in seconds - """ - length = 0 - info = self.bus.wait_for_response( - Message('mycroft.audio.service.get_track_length'), - timeout=1) - if info: - length = info.data.get("length") or 0 - return length / 1000 # convert to seconds - def get_track_position(self): - """ - get current position in seconds - """ - pos = 0 - info = self.bus.wait_for_response( - Message('mycroft.audio.service.get_track_position'), - timeout=1) - if info: - pos = info.data.get("position") or 0 - return pos / 1000 # convert to seconds +LOG.warning("ClassicAudioServiceInterface and OCPInterface moved to ovos_bus_client.apis.ocp") - def set_track_position(self, seconds): - """Seek X seconds. - - Arguments: - seconds (int): number of seconds to seek, if negative rewind +try: + from ovos_bus_client.apis.ocp import OCPInterface, ClassicAudioServiceInterface, ensure_uri +except ImportError: + from os.path import abspath + from ovos_utils.messagebus import get_mycroft_bus, dig_for_message, FakeMessage as Message + + def ensure_uri(s: str): """ - self.bus.emit(Message('mycroft.audio.service.set_track_position', - {"position": seconds * 1000})) # convert to ms - - def seek(self, seconds=1): - """Seek X seconds. - + Interpret paths as file:// uri's. + Args: - seconds (int): number of seconds to seek, if negative rewind + s: string path to be checked + + Returns: + if s is uri, s is returned otherwise file:// is prepended """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - if seconds < 0: - self.seek_backward(abs(seconds)) + if isinstance(s, str): + if '://' not in s: + return 'file://' + abspath(s) + else: + return s + elif isinstance(s, (tuple, list)): # Handle (mime, uri) arg + if '://' not in s[0]: + return 'file://' + abspath(s[0]), s[1] + else: + return s else: - self.seek_forward(seconds) - - def seek_forward(self, seconds=1): - """Skip ahead X seconds. - + raise ValueError('Invalid track') + + + class ClassicAudioServiceInterface: + """AudioService class for interacting with the audio subsystem + + Audio is managed by OCP in the default implementation, + usually this class should not be directly used, see OCPInterface instead + Args: - seconds (int): number of seconds to skip + bus: Mycroft messagebus connection """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - self.bus.emit(Message('mycroft.audio.service.seek_forward', - {"seconds": seconds})) - - def seek_backward(self, seconds=1): - """Rewind X seconds - - Args: - seconds (int): number of seconds to rewind - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - self.bus.emit(Message('mycroft.audio.service.seek_backward', - {"seconds": seconds})) - - def track_info(self): - """Request information of current playing track. - - Returns: - Dict with track info. - """ - info = self.bus.wait_for_response( - Message('mycroft.audio.service.track_info'), - reply_type='mycroft.audio.service.track_info_reply', - timeout=1) - return info.data if info else {} - - def available_backends(self): - """Return available audio backends. - - Returns: - dict with backend names as keys + + def __init__(self, bus=None): + self.bus = bus or get_mycroft_bus() + + def queue(self, tracks=None): + """Queue up a track to playing playlist. + + Args: + tracks: track uri or list of track uri's + Each track can be added as a tuple with (uri, mime) + to give a hint of the mime type to the system + """ + tracks = tracks or [] + if isinstance(tracks, (str, tuple)): + tracks = [tracks] + elif not isinstance(tracks, list): + raise ValueError + tracks = [ensure_uri(t) for t in tracks] + self.bus.emit(Message('mycroft.audio.service.queue', + data={'tracks': tracks})) + + def play(self, tracks=None, utterance=None, repeat=None): + """Start playback. + + Args: + tracks: track uri or list of track uri's + Each track can be added as a tuple with (uri, mime) + to give a hint of the mime type to the system + utterance: forward utterance for further processing by the + audio service. + repeat: if the playback should be looped + """ + repeat = repeat or False + tracks = tracks or [] + utterance = utterance or '' + if isinstance(tracks, (str, tuple)): + tracks = [tracks] + elif not isinstance(tracks, list): + raise ValueError + tracks = [ensure_uri(t) for t in tracks] + self.bus.emit(Message('mycroft.audio.service.play', + data={'tracks': tracks, + 'utterance': utterance, + 'repeat': repeat})) + + def stop(self): + """Stop the track.""" + self.bus.emit(Message('mycroft.audio.service.stop')) + + def next(self): + """Change to next track.""" + self.bus.emit(Message('mycroft.audio.service.next')) + + def prev(self): + """Change to previous track.""" + self.bus.emit(Message('mycroft.audio.service.prev')) + + def pause(self): + """Pause playback.""" + self.bus.emit(Message('mycroft.audio.service.pause')) + + def resume(self): + """Resume paused playback.""" + self.bus.emit(Message('mycroft.audio.service.resume')) + + def get_track_length(self): + """ + getting the duration of the audio in seconds + """ + length = 0 + info = self.bus.wait_for_response( + Message('mycroft.audio.service.get_track_length'), + timeout=1) + if info: + length = info.data.get("length") or 0 + return length / 1000 # convert to seconds + + def get_track_position(self): + """ + get current position in seconds + """ + pos = 0 + info = self.bus.wait_for_response( + Message('mycroft.audio.service.get_track_position'), + timeout=1) + if info: + pos = info.data.get("position") or 0 + return pos / 1000 # convert to seconds + + def set_track_position(self, seconds): + """Seek X seconds. + + Arguments: + seconds (int): number of seconds to seek, if negative rewind + """ + self.bus.emit(Message('mycroft.audio.service.set_track_position', + {"position": seconds * 1000})) # convert to ms + + def seek(self, seconds=1): + """Seek X seconds. + + Args: + seconds (int): number of seconds to seek, if negative rewind + """ + if isinstance(seconds, timedelta): + seconds = seconds.total_seconds() + if seconds < 0: + self.seek_backward(abs(seconds)) + else: + self.seek_forward(seconds) + + def seek_forward(self, seconds=1): + """Skip ahead X seconds. + + Args: + seconds (int): number of seconds to skip + """ + if isinstance(seconds, timedelta): + seconds = seconds.total_seconds() + self.bus.emit(Message('mycroft.audio.service.seek_forward', + {"seconds": seconds})) + + def seek_backward(self, seconds=1): + """Rewind X seconds + + Args: + seconds (int): number of seconds to rewind + """ + if isinstance(seconds, timedelta): + seconds = seconds.total_seconds() + self.bus.emit(Message('mycroft.audio.service.seek_backward', + {"seconds": seconds})) + + def track_info(self): + """Request information of current playing track. + + Returns: + Dict with track info. + """ + info = self.bus.wait_for_response( + Message('mycroft.audio.service.track_info'), + reply_type='mycroft.audio.service.track_info_reply', + timeout=1) + return info.data if info else {} + + def available_backends(self): + """Return available audio backends. + + Returns: + dict with backend names as keys + """ + msg = Message('mycroft.audio.service.list_backends') + response = self.bus.wait_for_response(msg) + return response.data if response else {} + + @property + def is_playing(self): + """True if the audioservice is playing, else False.""" + return self.track_info() != {} + + + class OCPInterface: + """bus api interface for OCP subsystem + Args: + bus: Mycroft messagebus connection """ - msg = Message('mycroft.audio.service.list_backends') - response = self.bus.wait_for_response(msg) - return response.data if response else {} + + def __init__(self, bus=None): + self.bus = bus or get_mycroft_bus() + + def _format_msg(self, msg_type, msg_data=None): + # this method ensures all skills are .forward from the utterance + # that triggered the skill, this ensures proper routing and metadata + msg_data = msg_data or {} + msg = dig_for_message() + if msg: + msg = msg.forward(msg_type, msg_data) + else: + msg = Message(msg_type, msg_data) + # at this stage source == skills, lets indicate audio service took over + sauce = msg.context.get("source") + if sauce == "skills": + msg.context["source"] = "audio_service" + return msg + + # OCP bus api + def queue(self, tracks): + """Queue up a track to OCP playing playlist. + + Args: + tracks: track dict or list of track dicts (OCP result style) + """ + + assert isinstance(tracks, list) + assert all(isinstance(t, dict) for t in tracks) + + msg = self._format_msg('ovos.common_play.playlist.queue', + {'tracks': tracks}) + self.bus.emit(msg) + + def play(self, tracks, utterance=None): + """Start playback. + Args: + tracks: track dict or list of track dicts (OCP result style) + utterance: forward utterance for further processing by OCP + + TODO handle utterance: + - allow services to register .voc files + - match utterance against vocs in OCP + - select audio service based on parsing + eg. "play X in spotify" + """ + assert isinstance(tracks, list) + assert all(isinstance(t, dict) for t in tracks) + + utterance = utterance or '' + + msg = self._format_msg('ovos.common_play.play', + {"media": tracks[0], + "playlist": tracks, + "utterance": utterance}) + self.bus.emit(msg) - @property - def is_playing(self): - """True if the audioservice is playing, else False.""" - return self.track_info() != {} class AudioServiceInterface(ClassicAudioServiceInterface): @@ -401,51 +466,3 @@ def available_backends(self): msg = self._format_msg('ovos.common_play.list_backends') response = self.bus.wait_for_response(msg) return response.data if response else {} - - -class OCPInterface(AudioServiceInterface): - """bus api interface for OCP subsystem - Args: - bus: Mycroft messagebus connection - """ - - def __init__(self, bus=None): - self.bus = bus or get_mycroft_bus() - - # OCP bus api - def queue(self, tracks): - """Queue up a track to OCP playing playlist. - - Args: - tracks: track dict or list of track dicts (OCP result style) - """ - - assert isinstance(tracks, list) - assert all(isinstance(t, dict) for t in tracks) - - msg = self._format_msg('ovos.common_play.playlist.queue', - {'tracks': tracks}) - self.bus.emit(msg) - - def play(self, tracks, utterance=None): - """Start playback. - Args: - tracks: track dict or list of track dicts (OCP result style) - utterance: forward utterance for further processing by OCP - - TODO handle utterance: - - allow services to register .voc files - - match utterance against vocs in OCP - - select audio service based on parsing - eg. "play X in spotify" - """ - assert isinstance(tracks, list) - assert all(isinstance(t, dict) for t in tracks) - - utterance = utterance or '' - - msg = self._format_msg('ovos.common_play.play', - {"media": tracks[0], - "playlist": tracks, - "utterance": utterance}) - self.bus.emit(msg) diff --git a/ovos_utils/skills/locations.py b/ovos_utils/skills/locations.py index c6a2d9f0..f3296916 100644 --- a/ovos_utils/skills/locations.py +++ b/ovos_utils/skills/locations.py @@ -1,149 +1,155 @@ -from os.path import join, isdir, dirname, expanduser, isfile from os import makedirs, listdir +from os.path import join, isdir, dirname, expanduser, isfile from typing import List, Optional + from ovos_config.config import read_mycroft_config from ovos_config.locations import get_xdg_data_save_path, get_xdg_data_dirs + from ovos_utils.log import LOG, log_deprecation +LOG.warning("ovos_utils.skills.locations moved to ovos_plugin_manager.skills") -def get_installed_skill_ids(conf: Optional[dict] = None) -> List[str]: - """ - Gets a list of `skill_id`s for all installed skills - Args: - conf: Configuration, else loads from ovos_config.config.Configuration - Returns: - list of `skill_id` strings for all installed skills - """ - _, skill_ids = get_plugin_skills() - for d in get_skill_directories(conf): - for skill_dir in listdir(d): - if isdir(join(d, skill_dir)) and isfile(join(d, skill_dir, - "__init__.py")): - if skill_dir in skill_ids: - LOG.info(f"{skill_dir} installed as plugin and local dir") - continue - skill_ids.append(skill_dir) - return skill_ids +try: + from ovos_plugin_manager.skills import * +except: + def get_installed_skill_ids(conf: Optional[dict] = None) -> List[str]: + """ + Gets a list of `skill_id`s for all installed skills + Args: + conf: Configuration, else loads from ovos_config.config.Configuration + Returns: + list of `skill_id` strings for all installed skills + """ + _, skill_ids = get_plugin_skills() + for d in get_skill_directories(conf): + for skill_dir in listdir(d): + if isdir(join(d, skill_dir)) and isfile(join(d, skill_dir, + "__init__.py")): + if skill_dir in skill_ids: + LOG.info(f"{skill_dir} installed as plugin and local dir") + continue + skill_ids.append(skill_dir) + return skill_ids -def get_skill_directories(conf: Optional[dict] = None) -> List[str]: - """ returns list of skill directories ordered by expected loading order - This corresponds to: - - XDG_DATA_DIRS - - default directory (see get_default_skills_directory method for details) - - user defined extra directories - Each directory contains individual skill folders to be loaded - If a skill exists in more than one directory (same folder name) previous instances will be ignored - ie. directories at the end of the list have priority over earlier directories - NOTE: empty folders are interpreted as disabled skills - new directories can be defined in mycroft.conf by specifying a full path - each extra directory is expected to contain individual skill folders to be loaded - the xdg folder name can also be changed, it defaults to "skills" - eg. ~/.local/share/mycroft/FOLDER_NAME - { - "skills": { - "directory": "skills", - "extra_directories": ["path/to/extra/dir/to/scan/for/skills"] + def get_skill_directories(conf: Optional[dict] = None) -> List[str]: + """ returns list of skill directories ordered by expected loading order + This corresponds to: + - XDG_DATA_DIRS + - default directory (see get_default_skills_directory method for details) + - user defined extra directories + Each directory contains individual skill folders to be loaded + If a skill exists in more than one directory (same folder name) previous instances will be ignored + ie. directories at the end of the list have priority over earlier directories + NOTE: empty folders are interpreted as disabled skills + new directories can be defined in mycroft.conf by specifying a full path + each extra directory is expected to contain individual skill folders to be loaded + the xdg folder name can also be changed, it defaults to "skills" + eg. ~/.local/share/mycroft/FOLDER_NAME + { + "skills": { + "directory": "skills", + "extra_directories": ["path/to/extra/dir/to/scan/for/skills"] + } } - } - Args: - conf: Configuration, else loads from ovos_config.config.Configuration - Returns: - list of fully-qualified directories containing non-plugin skills - """ - # the contents of each skills directory must be individual skill folders - # we are still dependent on the mycroft-core structure of skill_id/__init__.py + Args: + conf: Configuration, else loads from ovos_config.config.Configuration + Returns: + list of fully-qualified directories containing non-plugin skills + """ + # the contents of each skills directory must be individual skill folders + # we are still dependent on the mycroft-core structure of skill_id/__init__.py - conf = conf or read_mycroft_config() - folder = conf["skills"].get("directory") or "skills" - # load all valid XDG paths - # NOTE: skills are actually code, but treated as user data! - # they should be considered applets rather than full applications - skill_locations = list(reversed( - [join(p, folder) for p in get_xdg_data_dirs() if - isdir(join(p, folder))] - )) + conf = conf or read_mycroft_config() + folder = conf["skills"].get("directory") or "skills" + # load all valid XDG paths + # NOTE: skills are actually code, but treated as user data! + # they should be considered applets rather than full applications + skill_locations = list(reversed( + [join(p, folder) for p in get_xdg_data_dirs() if + isdir(join(p, folder))] + )) - # load the default skills folder - # only meaningful if xdg support is disabled - default = get_default_skills_directory(conf) - if default not in skill_locations: - skill_locations.append(default) + # load the default skills folder + # only meaningful if xdg support is disabled + default = get_default_skills_directory(conf) + if default not in skill_locations: + skill_locations.append(default) - # load additional explicitly configured directories - conf = conf.get("skills") or {} - # extra_directories is a list of directories containing skill subdirectories - # NOT a list of individual skill folders - # preserve order while removing any duplicate entries - extra_dirs = (expanduser(d) for d in conf.get("extra_directories") or []) - for d in extra_dirs: - if isdir(d) and d not in skill_locations: - skill_locations.append(d) - return skill_locations + # load additional explicitly configured directories + conf = conf.get("skills") or {} + # extra_directories is a list of directories containing skill subdirectories + # NOT a list of individual skill folders + # preserve order while removing any duplicate entries + extra_dirs = (expanduser(d) for d in conf.get("extra_directories") or []) + for d in extra_dirs: + if isdir(d) and d not in skill_locations: + skill_locations.append(d) + return skill_locations -def get_default_skills_directory(conf: Optional[dict] = None) -> str: - """ return default directory to scan for skills - This is only meaningful if xdg is disabled in ovos.conf - If xdg is enabled then data_dir is always XDG_DATA_DIR - If xdg is disabled then data_dir by default corresponds to /opt/mycroft - users can define the data directory in mycroft.conf - the skills folder name (relative to data_dir) can also be defined there - NOTE: folder name also impacts all XDG skill directories! - { - "data_dir": "/opt/mycroft", - "skills": { - "directory": "skills" + def get_default_skills_directory(conf: Optional[dict] = None) -> str: + """ return default directory to scan for skills + This is only meaningful if xdg is disabled in ovos.conf + If xdg is enabled then data_dir is always XDG_DATA_DIR + If xdg is disabled then data_dir by default corresponds to /opt/mycroft + users can define the data directory in mycroft.conf + the skills folder name (relative to data_dir) can also be defined there + NOTE: folder name also impacts all XDG skill directories! + { + "data_dir": "/opt/mycroft", + "skills": { + "directory": "skills" + } } - } - Args: - conf: Configuration, else loads from ovos_config.config.Configuration - Returns: - Absolute path to default skills directory - """ - conf = conf or read_mycroft_config() - path_override = conf["skills"].get("directory_override") - folder = conf["skills"].get("directory") or "skills" + Args: + conf: Configuration, else loads from ovos_config.config.Configuration + Returns: + Absolute path to default skills directory + """ + conf = conf or read_mycroft_config() + path_override = conf["skills"].get("directory_override") + folder = conf["skills"].get("directory") or "skills" - # if .conf wants to use a specific path, use it! - if path_override: - log_deprecation("'directory_override' is deprecated!" - "add the new path to 'extra_directories' instead", - "0.1.0") - skills_folder = expanduser(path_override) - elif conf["skills"].get("extra_directories") and \ - len(conf["skills"].get("extra_directories")) > 0: - skills_folder = expanduser(conf["skills"]["extra_directories"][0]) - else: - skills_folder = join(get_xdg_data_save_path(), folder) - # create folder if needed - try: - makedirs(skills_folder, exist_ok=True) - except PermissionError: # old style /opt/mycroft/skills not available - skills_folder = join(get_xdg_data_save_path(), folder) - makedirs(skills_folder, exist_ok=True) + # if .conf wants to use a specific path, use it! + if path_override: + log_deprecation("'directory_override' is deprecated!" + "add the new path to 'extra_directories' instead", + "0.1.0") + skills_folder = expanduser(path_override) + elif conf["skills"].get("extra_directories") and \ + len(conf["skills"].get("extra_directories")) > 0: + skills_folder = expanduser(conf["skills"]["extra_directories"][0]) + else: + skills_folder = join(get_xdg_data_save_path(), folder) + # create folder if needed + try: + makedirs(skills_folder, exist_ok=True) + except PermissionError: # old style /opt/mycroft/skills not available + skills_folder = join(get_xdg_data_save_path(), folder) + makedirs(skills_folder, exist_ok=True) - return skills_folder + return skills_folder -def get_plugin_skills() -> (list, list): - """ - Get the package directories for any pip installed skill plugins - Returns: - lists of skill directories and plugin skill IDs - """ - import importlib.util - try: - from ovos_plugin_manager.skills import find_skill_plugins - except ImportError: - LOG.warning("ovos-plugin-manager not available to load plugin skills") - return [], [] - skill_dirs = list() - plugins = find_skill_plugins() - skill_ids = list(plugins.keys()) - for skill_class in plugins.values(): - skill_dir = dirname(importlib.util.find_spec( - skill_class.__module__).origin) - skill_dirs.append(skill_dir) - LOG.info(f"Located plugin skills: {skill_ids}") - return skill_dirs, skill_ids + def get_plugin_skills() -> (list, list): + """ + Get the package directories for any pip installed skill plugins + Returns: + lists of skill directories and plugin skill IDs + """ + import importlib.util + try: + from ovos_plugin_manager.skills import find_skill_plugins + except ImportError: + LOG.warning("ovos-plugin-manager not available to load plugin skills") + return [], [] + skill_dirs = list() + plugins = find_skill_plugins() + skill_ids = list(plugins.keys()) + for skill_class in plugins.values(): + skill_dir = dirname(importlib.util.find_spec( + skill_class.__module__).origin) + skill_dirs.append(skill_dir) + LOG.info(f"Located plugin skills: {skill_ids}") + return skill_dirs, skill_ids diff --git a/ovos_utils/skills/settings.py b/ovos_utils/skills/settings.py index d6f439ea..e44a49a8 100644 --- a/ovos_utils/skills/settings.py +++ b/ovos_utils/skills/settings.py @@ -1,60 +1,69 @@ -import requests import json from os.path import join, expanduser, exists + +import requests from json_database import JsonStorageXDG, JsonStorage -from ovos_utils.log import LOG, log_deprecation - - -def settings2meta(settings, section_name="Skill Settings"): - """ generates basic settingsmeta """ - fields = [] - - for k, v in settings.items(): - if k.startswith("_"): - continue - label = k.replace("-", " ").replace("_", " ").title() - if isinstance(v, bool): - fields.append({ - "name": k, - "type": "checkbox", - "label": label, - "value": str(v).lower() - }) - if isinstance(v, str): - fields.append({ - "name": k, - "type": "text", - "label": label, - "value": v - }) - if isinstance(v, int): - fields.append({ - "name": k, - "type": "number", - "label": label, - "value": str(v) - }) - return { - "skillMetadata": { - "sections": [ - { - "name": section_name, - "fields": fields - } - ] + +from ovos_utils.log import LOG, log_deprecation, deprecated + +LOG.warning("ovos_utils.skills.settings moved to ovos_workshop.settings") + +try: + from ovos_workshop.settings import * + +except ImportError: + + def settings2meta(settings, section_name="Skill Settings"): + """ generates basic settingsmeta """ + fields = [] + + for k, v in settings.items(): + if k.startswith("_"): + continue + label = k.replace("-", " ").replace("_", " ").title() + if isinstance(v, bool): + fields.append({ + "name": k, + "type": "checkbox", + "label": label, + "value": str(v).lower() + }) + if isinstance(v, str): + fields.append({ + "name": k, + "type": "text", + "label": label, + "value": v + }) + if isinstance(v, int): + fields.append({ + "name": k, + "type": "number", + "label": label, + "value": str(v) + }) + return { + "skillMetadata": { + "sections": [ + { + "name": section_name, + "fields": fields + } + ] + } } - } -class PrivateSettings(JsonStorageXDG): - def __init__(self, skill_id): - super(PrivateSettings, self).__init__(skill_id) + class PrivateSettings(JsonStorageXDG): + def __init__(self, skill_id): + super(PrivateSettings, self).__init__(skill_id) - @property - def settingsmeta(self): - return settings2meta(self, self.name) + @property + def settingsmeta(self): + return settings2meta(self, self.name) +@deprecated("deprecated without replacement, selene backend is dead", "0.1.0") def get_remote_settings(skill_id, identity_file=None, backend_url=None): """ WARNING: selene backend does not use proper skill_id, if you have skills with same name but different author settings will overwrite each @@ -71,6 +80,7 @@ def get_remote_settings(skill_id, identity_file=None, backend_url=None): return data +@deprecated("deprecated without replacement, selene backend is dead", "0.1.0") def get_all_remote_settings(identity_file=None, backend_url=None): """ WARNING: selene backend does not use proper skill_id, if you have skills with same name but different author settings will overwrite each @@ -90,6 +100,7 @@ def get_all_remote_settings(identity_file=None, backend_url=None): return requests.get(url, headers=params).json() +@deprecated("deprecated without replacement, skill settings no longer shipped in skill folder", "0.1.0") def get_local_settings(skill_dir, skill_name=None) -> dict: """Build a JsonStorage using the JSON string stored in settings.json.""" if skill_name: @@ -102,6 +113,7 @@ def get_local_settings(skill_dir, skill_name=None) -> dict: return JsonStorage(settings_path) +@deprecated("deprecated without replacement, skill settings no longer shipped in skill folder", "0.1.0") def save_settings(skill_dir, skill_settings): """Save skill settings to file.""" if skill_dir.endswith("/settings.json"): @@ -117,4 +129,4 @@ def save_settings(skill_dir, skill_settings): except Exception: LOG.error(f'error saving skill settings to {settings_path}') else: - LOG.info(f'Skill settings successfully saved to {settings_path}') \ No newline at end of file + LOG.info(f'Skill settings successfully saved to {settings_path}') diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 00000000..8c750061 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,3 @@ +ovos_plugin_manager>=0.0.25a2 +ovos-config>=0.0.12a6 +ovos-workshop>=0.0.13a22 \ No newline at end of file diff --git a/test/unittests/test_skills.py b/test/unittests/test_skills.py index 4cb58d55..dadc6257 100644 --- a/test/unittests/test_skills.py +++ b/test/unittests/test_skills.py @@ -69,7 +69,7 @@ def test_ocp_interface(self): class TestLocations(unittest.TestCase): - @patch("ovos_utils.skills.locations.get_plugin_skills") + @patch("ovos_plugin_manager.skills.get_plugin_skills") def test_get_installed_skill_ids(self, plugins): plugins.return_value = (['plugin_dir', 'plugin_dir_2'], ['plugin_id', 'plugin_id_2']) From c97cc4b3bdc8549e911429a01d855f621ede8ef9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 20:51:18 +0000 Subject: [PATCH 08/15] Increment Version to 0.0.37a3 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 0674a6a8..326c0ba2 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 37 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK From f9f0812b2730e24baaf8170a9f4e19bb633d7b14 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 20:51:48 +0000 Subject: [PATCH 09/15] Update Changelog --- CHANGELOG.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8570468..d08df712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ # Changelog -## [0.0.37a2](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a2) (2023-12-18) +## [0.0.37a3](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a3) (2023-12-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a1...0.0.37a2) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a2...0.0.37a3) + +**Closed issues:** + +- decouple concerns from bus/workshop [\#205](https://github.com/OpenVoiceOS/ovos-utils/issues/205) +- ROADMAP - ovos-utils 0.1.0 [\#117](https://github.com/OpenVoiceOS/ovos-utils/issues/117) + +**Merged pull requests:** + +- LAST ALPHA [\#206](https://github.com/OpenVoiceOS/ovos-utils/pull/206) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.37a2](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a2) (2023-12-18) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a1...V0.0.37a2) **Fixed bugs:** From 92f80e145107759ea04f5fa9bd511bfc57f22885 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:53:11 +0000 Subject: [PATCH 10/15] Update coverage.yml --- .github/workflows/coverage.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8a967a1a..fc2630fb 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,6 +23,7 @@ jobs: python -m pip install build wheel - name: Install repo run: | + pip install -r requirements/test.txt pip install -e .[extras] - name: Generate coverage report run: | @@ -38,4 +39,4 @@ jobs: files: ./coverage.xml,!./cache flags: unittests name: codecov-umbrella - verbose: true \ No newline at end of file + verbose: true From a6432fbd2fcc53d8d487d6cf99f0c0ee603f3ef0 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 28 Dec 2023 23:25:13 +0000 Subject: [PATCH 11/15] deprecate bus utils (#207) * deprecate bus utils * deprecate bus utils --- ovos_utils/enclosure/__init__.py | 4 +- ovos_utils/enclosure/api.py | 5 +- ovos_utils/enclosure/mark1/__init__.py | 5 +- ovos_utils/enclosure/mark1/eyes/__init__.py | 6 +- .../enclosure/mark1/faceplate/__init__.py | 5 +- .../enclosure/mark1/faceplate/animations.py | 6 +- .../mark1/faceplate/cellular_automaton.py | 6 +- ovos_utils/enclosure/mark1/faceplate/icons.py | 6 +- ovos_utils/intents/__init__.py | 4 +- ovos_utils/intents/converse.py | 4 +- .../intents/intent_service_interface.py | 3 +- ovos_utils/intents/layers.py | 4 +- ovos_utils/messagebus.py | 353 +++++++++--------- ovos_utils/skills/api.py | 4 +- ovos_utils/skills/audioservice.py | 4 +- ovos_utils/skills/locations.py | 2 +- ovos_utils/skills/settings.py | 2 +- 17 files changed, 214 insertions(+), 209 deletions(-) diff --git a/ovos_utils/enclosure/__init__.py b/ovos_utils/enclosure/__init__.py index bbebc37f..e9135711 100644 --- a/ovos_utils/enclosure/__init__.py +++ b/ovos_utils/enclosure/__init__.py @@ -3,9 +3,9 @@ from enum import Enum from os.path import exists from typing import Optional -from ovos_utils.log import LOG, deprecated +from ovos_utils.log import deprecated, log_deprecation -LOG.warning("ovos_utils.enclosure has been deprecated! this module will be removed in version 0.1.0") +log_deprecation("ovos_utils.enclosure has been deprecated!", "0.1.0") class MycroftEnclosures(str, Enum): diff --git a/ovos_utils/enclosure/api.py b/ovos_utils/enclosure/api.py index 7f48e48e..c3c2f59a 100644 --- a/ovos_utils/enclosure/api.py +++ b/ovos_utils/enclosure/api.py @@ -1,6 +1,7 @@ -from ovos_utils.log import LOG +from ovos_utils.log import log_deprecation + +log_deprecation("EnclosureApi has moved to ovos_bus_client.apis.enclosure", "0.1.0") -LOG.warning("EnclosureApi has moved to ovos_bus_client.apis.enclosure") try: from ovos_bus_client.apis.enclosure import EnclosureApi diff --git a/ovos_utils/enclosure/mark1/__init__.py b/ovos_utils/enclosure/mark1/__init__.py index 5e4766ac..6f51bbdd 100644 --- a/ovos_utils/enclosure/mark1/__init__.py +++ b/ovos_utils/enclosure/mark1/__init__.py @@ -1,8 +1,7 @@ from ovos_utils.enclosure.api import EnclosureAPI -from ovos_utils.log import LOG +from ovos_utils.log import log_deprecation -LOG.warning("ovos_utils.enclosure.mark1 moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" - " this module will be removed in version 0.1.0") +log_deprecation("ovos_utils.enclosure.mark1 moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") class Mark1EnclosureAPI(EnclosureAPI): diff --git a/ovos_utils/enclosure/mark1/eyes/__init__.py b/ovos_utils/enclosure/mark1/eyes/__init__.py index 48743c48..334161eb 100644 --- a/ovos_utils/enclosure/mark1/eyes/__init__.py +++ b/ovos_utils/enclosure/mark1/eyes/__init__.py @@ -1,7 +1,7 @@ -from ovos_utils.log import LOG +from ovos_utils.log import log_deprecation + +log_deprecation("ovos_utils.enclosure.mark1.eyes moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") -LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" - " this module will be removed in version 0.1.0") try: from ovos_mark1.eyes import * diff --git a/ovos_utils/enclosure/mark1/faceplate/__init__.py b/ovos_utils/enclosure/mark1/faceplate/__init__.py index 64ebcce4..f6fedfbd 100644 --- a/ovos_utils/enclosure/mark1/faceplate/__init__.py +++ b/ovos_utils/enclosure/mark1/faceplate/__init__.py @@ -1,14 +1,13 @@ from ovos_utils.enclosure.mark1 import Mark1EnclosureAPI from ovos_utils import create_loop -from ovos_utils.log import LOG +from ovos_utils.log import LOG, log_deprecation from ovos_utils.messagebus import get_mycroft_bus import random from time import sleep from collections.abc import MutableSequence import copy -LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" - " this module will be removed in version 0.1.0") +log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") try: diff --git a/ovos_utils/enclosure/mark1/faceplate/animations.py b/ovos_utils/enclosure/mark1/faceplate/animations.py index 93042170..cc50e40f 100644 --- a/ovos_utils/enclosure/mark1/faceplate/animations.py +++ b/ovos_utils/enclosure/mark1/faceplate/animations.py @@ -1,10 +1,10 @@ import copy import random -from ovos_utils.log import LOG +from ovos_utils.log import log_deprecation + +log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") -LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" - " this module will be removed in version 0.1.0") try: from ovos_mark1.faceplate.animations import * diff --git a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py b/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py index 92d43245..23198546 100644 --- a/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py +++ b/ovos_utils/enclosure/mark1/faceplate/cellular_automaton.py @@ -1,10 +1,10 @@ import copy import random -from ovos_utils.log import LOG +from ovos_utils.log import log_deprecation + +log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") -LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" - " this module will be removed in version 0.1.0") try: from ovos_mark1.faceplate.cellular_automaton import * diff --git a/ovos_utils/enclosure/mark1/faceplate/icons.py b/ovos_utils/enclosure/mark1/faceplate/icons.py index 3db4ae83..f49c1e02 100644 --- a/ovos_utils/enclosure/mark1/faceplate/icons.py +++ b/ovos_utils/enclosure/mark1/faceplate/icons.py @@ -1,7 +1,7 @@ -from ovos_utils.log import LOG +from ovos_utils.log import log_deprecation + +log_deprecation("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils", "0.1.0") -LOG.warning("ovos_utils.enclosure.mark1.faceplate moved to https://github.com/OpenVoiceOS/ovos-mark1-utils ;" - " this module will be removed in version 0.1.0") try: from ovos_mark1.faceplate.icons import * diff --git a/ovos_utils/intents/__init__.py b/ovos_utils/intents/__init__.py index 2f14e69d..2d329971 100644 --- a/ovos_utils/intents/__init__.py +++ b/ovos_utils/intents/__init__.py @@ -2,9 +2,9 @@ IntentServiceInterface from ovos_utils.intents.converse import ConverseTracker from ovos_utils.intents.layers import IntentLayers -from ovos_utils.log import LOG +from ovos_utils.log import log_deprecation -LOG.warning("ovos_utils.intents moved to ovos_workshop.intents") +log_deprecation("ovos_utils.intents moved to ovos_workshop.intents", "0.1.0") try: from ovos_workshop.intents import * diff --git a/ovos_utils/intents/converse.py b/ovos_utils/intents/converse.py index 05b545a1..ca8995ea 100644 --- a/ovos_utils/intents/converse.py +++ b/ovos_utils/intents/converse.py @@ -2,9 +2,9 @@ import ovos_utils.messagebus from ovos_utils.intents.intent_service_interface import IntentQueryApi -from ovos_utils.log import LOG +from ovos_utils.log import LOG, log_deprecation -LOG.warning("ConverseTracker has been deprecated without replacement, it will be removed in 0.1.0") +log_deprecation("ConverseTracker has been deprecated without replacement", "0.1.0") class ConverseTracker: diff --git a/ovos_utils/intents/intent_service_interface.py b/ovos_utils/intents/intent_service_interface.py index db6448b2..6fd52a2d 100644 --- a/ovos_utils/intents/intent_service_interface.py +++ b/ovos_utils/intents/intent_service_interface.py @@ -5,8 +5,7 @@ import ovos_utils.messagebus from ovos_utils.log import LOG, log_deprecation -LOG.warning("ovos_utils.intents moved to ovos_workshop.intents") - +log_deprecation("ovos_utils.intents moved to ovos_workshop.intents", "0.1.0") try: from ovos_workshop.intents import * diff --git a/ovos_utils/intents/layers.py b/ovos_utils/intents/layers.py index a699bf62..56097604 100644 --- a/ovos_utils/intents/layers.py +++ b/ovos_utils/intents/layers.py @@ -1,6 +1,6 @@ -from ovos_utils.log import LOG +from ovos_utils.log import LOG, log_deprecation -LOG.warning("IntentLayers moved to ovos_workshop.decorators.layers") +log_deprecation("IntentLayers moved to ovos_workshop.decorators.layers", "0.1.0") try: from ovos_workshop.decorators.layers import IntentLayers diff --git a/ovos_utils/messagebus.py b/ovos_utils/messagebus.py index 6bea298e..5251ef93 100644 --- a/ovos_utils/messagebus.py +++ b/ovos_utils/messagebus.py @@ -1,7 +1,6 @@ import json import time from copy import deepcopy -from inspect import signature from threading import Event # from ovos_utils.configuration import read_mycroft_config, get_default_lang @@ -9,15 +8,11 @@ from ovos_utils import create_loop from ovos_utils.json_helper import merge_dict -from ovos_utils.log import LOG, log_deprecation -from ovos_utils.metrics import Stopwatch -from ovos_utils.events import EventContainer, get_handler_name, create_basic_wrapper, create_wrapper, unmunge_message +from ovos_utils.log import LOG, log_deprecation, deprecated - -_DEFAULT_WS_CONFIG = {"host": "0.0.0.0", - "port": 8181, - "route": "/core", - "ssl": False} +log_deprecation("decode_binary_message, send_binary_file_message, send_binary_data_message, \ + send_message, wait_for_reply, listen_once_for_message, get_message_lang, get_websocket, get_mycroft_bus, \ + listen_for_message have moved to ovos_bus_client.util", "0.1.0") def dig_for_message(): @@ -360,187 +355,195 @@ def __int__(self, *args, **kwargs): FakeMessage.__init__(self, *args, **kwargs) -def get_message_lang(message=None): - """Get the language from the message or the default language. - Args: - message: message to check for language code. - Returns: - The language code from the message or the default language. - """ - try: - from ovos_config.locale import get_default_lang - default_lang = get_default_lang() - except ImportError: - LOG.warning("ovos_config not available. Using default lang en-us") - default_lang = "en-us" - message = message or dig_for_message() - if not message: - return default_lang - lang = message.data.get("lang") or message.context.get("lang") or default_lang - return lang.lower() +try: + from ovos_bus_client.util import decode_binary_message, send_binary_file_message, send_binary_data_message, \ + send_message, wait_for_reply, listen_once_for_message, get_message_lang, get_websocket, get_mycroft_bus, \ + listen_for_message +except: + _DEFAULT_WS_CONFIG = {"host": "0.0.0.0", + "port": 8181, + "route": "/core", + "ssl": False} -def get_websocket(host, port, route='/', ssl=False, threaded=True): - """ - Returns a connection to a websocket - """ - from ovos_bus_client import MessageBusClient + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def get_message_lang(message=None): + """Get the language from the message or the default language. + Args: + message: message to check for language code. + Returns: + The language code from the message or the default language. + """ + try: + from ovos_config.locale import get_default_lang + default_lang = get_default_lang() + except ImportError: + LOG.warning("ovos_config not available. Using default lang en-us") + default_lang = "en-us" + message = message or dig_for_message() + if not message: + return default_lang + lang = message.data.get("lang") or message.context.get("lang") or default_lang + return lang.lower() - client = MessageBusClient(host, port, route, ssl) - if threaded: - client.run_in_thread() - return client + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def get_websocket(host, port, route='/', ssl=False, threaded=True): + """ + Returns a connection to a websocket + """ + from ovos_bus_client import MessageBusClient -def get_mycroft_bus(host: str = None, port: int = None, route: str = None, - ssl: bool = None): - """ - Returns a connection to the mycroft messagebus - """ - try: - from ovos_config.config import read_mycroft_config - config = read_mycroft_config().get('websocket') or dict() - except ImportError: - LOG.warning("ovos_config not available. Falling back to default WS") - config = dict() - host = host or config.get('host') or _DEFAULT_WS_CONFIG['host'] - port = port or config.get('port') or _DEFAULT_WS_CONFIG['port'] - route = route or config.get('route') or _DEFAULT_WS_CONFIG['route'] - if ssl is None: - ssl = config.get('ssl') if 'ssl' in config else \ - _DEFAULT_WS_CONFIG['ssl'] - return get_websocket(host, port, route, ssl) - - -def listen_for_message(msg_type, handler, bus=None): - """ - Continuously listens and reacts to a specific messagetype on the mycroft messagebus + client = MessageBusClient(host, port, route, ssl) + if threaded: + client.run_in_thread() + return client - NOTE: when finished you should call bus.remove(msg_type, handler) - """ - bus = bus or get_mycroft_bus() - bus.on(msg_type, handler) - return bus + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def get_mycroft_bus(host: str = None, port: int = None, route: str = None, + ssl: bool = None): + """ + Returns a connection to the mycroft messagebus + """ + try: + from ovos_config.config import read_mycroft_config + config = read_mycroft_config().get('websocket') or dict() + except ImportError: + LOG.warning("ovos_config not available. Falling back to default WS") + config = dict() + host = host or config.get('host') or _DEFAULT_WS_CONFIG['host'] + port = port or config.get('port') or _DEFAULT_WS_CONFIG['port'] + route = route or config.get('route') or _DEFAULT_WS_CONFIG['route'] + if ssl is None: + ssl = config.get('ssl') if 'ssl' in config else \ + _DEFAULT_WS_CONFIG['ssl'] + return get_websocket(host, port, route, ssl) + + + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def listen_for_message(msg_type, handler, bus=None): + """ + Continuously listens and reacts to a specific messagetype on the mycroft messagebus -def listen_once_for_message(msg_type, handler, bus=None): - """ - listens and reacts once to a specific messagetype on the mycroft messagebus - """ - auto_close = bus is None - bus = bus or get_mycroft_bus() + NOTE: when finished you should call bus.remove(msg_type, handler) + """ + bus = bus or get_mycroft_bus() + bus.on(msg_type, handler) + return bus - def _handler(message): - handler(message) - if auto_close: - bus.close() - bus.once(msg_type, _handler) - return bus + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def listen_once_for_message(msg_type, handler, bus=None): + """ + listens and reacts once to a specific messagetype on the mycroft messagebus + """ + auto_close = bus is None + bus = bus or get_mycroft_bus() + def _handler(message): + handler(message) + if auto_close: + bus.close() -def wait_for_reply(message, reply_type=None, timeout=3.0, bus=None): - """Send a message and wait for a response. + bus.once(msg_type, _handler) + return bus - Args: - message (FakeMessage or str or dict): message object or type to send - reply_type (str): the message type of the expected reply. - Defaults to ".response". - timeout: seconds to wait before timeout, defaults to 3 - Returns: - The received message or None if the response timed out - """ - auto_close = bus is None - bus = bus or get_mycroft_bus() - if isinstance(message, str): - try: - message = json.loads(message) - except: - pass - if isinstance(message, str): - message = FakeMessage(message) - elif isinstance(message, dict): - message = FakeMessage(message["type"], - message.get("data"), - message.get("context")) - elif not isinstance(message, FakeMessage): - raise ValueError - response = bus.wait_for_response(message, reply_type, timeout) - if auto_close: - bus.close() - return response - - -def send_message(message, data=None, context=None, bus=None): - auto_close = bus is None - bus = bus or get_mycroft_bus() - if isinstance(message, str): - if isinstance(data, dict) or isinstance(context, dict): - message = FakeMessage(message, data, context) - else: + + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def wait_for_reply(message, reply_type=None, timeout=3.0, bus=None): + """Send a message and wait for a response. + + Args: + message (FakeMessage or str or dict): message object or type to send + reply_type (str): the message type of the expected reply. + Defaults to ".response". + timeout: seconds to wait before timeout, defaults to 3 + Returns: + The received message or None if the response timed out + """ + auto_close = bus is None + bus = bus or get_mycroft_bus() + if isinstance(message, str): try: message = json.loads(message) except: - message = FakeMessage(message) - if isinstance(message, dict): - message = FakeMessage(message["type"], - message.get("data"), - message.get("context")) - if not isinstance(message, FakeMessage): - raise ValueError - bus.emit(message) - if auto_close: - bus.close() - - -def send_binary_data_message(binary_data, msg_type="mycroft.binary.data", - msg_data=None, msg_context=None, bus=None): - msg_data = msg_data or {} - msg = { - "type": msg_type, - "data": merge_dict(msg_data, {"binary": binary_data.hex()}), - "context": msg_context or None - } - send_message(msg, bus=bus) - - -def send_binary_file_message(filepath, msg_type="mycroft.binary.file", - msg_context=None, bus=None): - with open(filepath, 'rb') as f: - binary_data = f.read() - msg_data = {"path": filepath} - send_binary_data_message(binary_data, msg_type=msg_type, msg_data=msg_data, - msg_context=msg_context, bus=bus) - - -def decode_binary_message(message): - if isinstance(message, str): - try: # json string - message = json.loads(message) + pass + if isinstance(message, str): + message = FakeMessage(message) + elif isinstance(message, dict): + message = FakeMessage(message["type"], + message.get("data"), + message.get("context")) + elif not isinstance(message, FakeMessage): + raise ValueError + response = bus.wait_for_response(message, reply_type, timeout) + if auto_close: + bus.close() + return response + + + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def send_message(message, data=None, context=None, bus=None): + auto_close = bus is None + bus = bus or get_mycroft_bus() + if isinstance(message, str): + if isinstance(data, dict) or isinstance(context, dict): + message = FakeMessage(message, data, context) + else: + try: + message = json.loads(message) + except: + message = FakeMessage(message) + if isinstance(message, dict): + message = FakeMessage(message["type"], + message.get("data"), + message.get("context")) + if not isinstance(message, FakeMessage): + raise ValueError + bus.emit(message) + if auto_close: + bus.close() + + + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def send_binary_data_message(binary_data, msg_type="mycroft.binary.data", + msg_data=None, msg_context=None, bus=None): + msg_data = msg_data or {} + msg = { + "type": msg_type, + "data": merge_dict(msg_data, {"binary": binary_data.hex()}), + "context": msg_context or None + } + send_message(msg, bus=bus) + + + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def send_binary_file_message(filepath, msg_type="mycroft.binary.file", + msg_context=None, bus=None): + with open(filepath, 'rb') as f: + binary_data = f.read() + msg_data = {"path": filepath} + send_binary_data_message(binary_data, msg_type=msg_type, msg_data=msg_data, + msg_context=msg_context, bus=bus) + + + @deprecated("moved to ovos_bus_client.util", "0.1.0") + def decode_binary_message(message): + if isinstance(message, str): + try: # json string + message = json.loads(message) + binary_data = message.get("binary") or message["data"]["binary"] + except: # hex string + binary_data = message + elif isinstance(message, dict): + # data field or serialized message binary_data = message.get("binary") or message["data"]["binary"] - except: # hex string - binary_data = message - elif isinstance(message, dict): - # data field or serialized message - binary_data = message.get("binary") or message["data"]["binary"] - else: - # message object - binary_data = message.data["binary"] - # decode hex string - return bytearray.fromhex(binary_data) - - -def to_alnum(skill_id): - """Convert a skill id to only alphanumeric characters - - Non alpha-numeric characters are converted to "_" - - Args: - skill_id (str): identifier to be converted - Returns: - (str) String of letters - """ - return ''.join(c if c.isalnum() else '_' for c in str(skill_id)) + else: + # message object + binary_data = message.data["binary"] + # decode hex string + return bytearray.fromhex(binary_data) class BusService: @@ -557,6 +560,7 @@ class BusService: """ + @deprecated("deprecated without replacement", "0.1.0") def __init__(self, message, trigger_messages=None, bus=None): self.bus = bus or get_mycroft_bus() self.response = message @@ -604,6 +608,7 @@ def handle_get_time(self, message): """ + @deprecated("deprecated without replacement", "0.1.0") def __init__(self, trigger_message, name=None, bus=None, config=None): """ initialize responder @@ -691,6 +696,7 @@ class BusQuery: """ + @deprecated("deprecated without replacement", "0.1.0") def __init__(self, message, bus=None): self.bus = bus or get_mycroft_bus() self._waiting = False @@ -757,6 +763,7 @@ def __init__(self, name="clock_receiver", timeout=3, bus=None): """ + @deprecated("deprecated without replacement", "0.1.0") def __init__(self, query_message, name=None, timeout=5, bus=None, config=None): self.query_message = query_message diff --git a/ovos_utils/skills/api.py b/ovos_utils/skills/api.py index f85b0908..ee91f08c 100644 --- a/ovos_utils/skills/api.py +++ b/ovos_utils/skills/api.py @@ -1,6 +1,6 @@ -from ovos_utils.log import LOG +from ovos_utils.log import LOG, log_deprecation -LOG.warning("ovos_utils.skills.api moved to ovos_workshop.skills.api") +log_deprecation("ovos_utils.skills.api moved to ovos_workshop.skills.api", "0.1.0") try: from ovos_workshop.skills.api import SkillApi diff --git a/ovos_utils/skills/audioservice.py b/ovos_utils/skills/audioservice.py index 8c01c940..d583c95a 100644 --- a/ovos_utils/skills/audioservice.py +++ b/ovos_utils/skills/audioservice.py @@ -14,9 +14,9 @@ # from datetime import timedelta -from ovos_utils.log import LOG, deprecated +from ovos_utils.log import deprecated, log_deprecation -LOG.warning("ClassicAudioServiceInterface and OCPInterface moved to ovos_bus_client.apis.ocp") +log_deprecation("ClassicAudioServiceInterface and OCPInterface moved to ovos_bus_client.apis.ocp", "0.1.0") try: from ovos_bus_client.apis.ocp import OCPInterface, ClassicAudioServiceInterface, ensure_uri diff --git a/ovos_utils/skills/locations.py b/ovos_utils/skills/locations.py index f3296916..9ab84428 100644 --- a/ovos_utils/skills/locations.py +++ b/ovos_utils/skills/locations.py @@ -7,7 +7,7 @@ from ovos_utils.log import LOG, log_deprecation -LOG.warning("ovos_utils.skills.locations moved to ovos_plugin_manager.skills") +log_deprecation("ovos_utils.skills.locations moved to ovos_plugin_manager.skills", "0.1.0") try: from ovos_plugin_manager.skills import * diff --git a/ovos_utils/skills/settings.py b/ovos_utils/skills/settings.py index e44a49a8..1d476040 100644 --- a/ovos_utils/skills/settings.py +++ b/ovos_utils/skills/settings.py @@ -6,7 +6,7 @@ from ovos_utils.log import LOG, log_deprecation, deprecated -LOG.warning("ovos_utils.skills.settings moved to ovos_workshop.settings") +log_deprecation("ovos_utils.skills.settings moved to ovos_workshop.settings", "0.1.0") try: from ovos_workshop.settings import * From d5d2d504425c51d454d4a7a1e61fc756bdc0efe6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 23:25:28 +0000 Subject: [PATCH 12/15] Increment Version to 0.0.37a4 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 326c0ba2..64ef368c 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 37 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK From 165a5c1295a0d458a26cc8b8a3c4dd595f9bd90b Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 23:25:56 +0000 Subject: [PATCH 13/15] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d08df712..49f21eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.37a3](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a3) (2023-12-28) +## [0.0.37a4](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a4) (2023-12-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a2...0.0.37a3) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a3...0.0.37a4) + +**Merged pull requests:** + +- deprecate bus utils [\#207](https://github.com/OpenVoiceOS/ovos-utils/pull/207) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.37a3](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a3) (2023-12-28) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a2...V0.0.37a3) **Closed issues:** From 8f9c6a61041c6d3bdd74f6e85fd881f17ed37467 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 23:26:18 +0000 Subject: [PATCH 14/15] Increment Version to 0.0.37 --- ovos_utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_utils/version.py b/ovos_utils/version.py index 64ef368c..85700f73 100644 --- a/ovos_utils/version.py +++ b/ovos_utils/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 37 -VERSION_ALPHA = 4 +VERSION_ALPHA = 0 # END_VERSION_BLOCK From cb5a33c614d0170b3559caef5c826247fc8303ab Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Dec 2023 23:26:53 +0000 Subject: [PATCH 15/15] Update Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f21eab..e64070d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [0.0.37a4](https://github.com/OpenVoiceOS/ovos-utils/tree/0.0.37a4) (2023-12-28) +## [V0.0.37a4](https://github.com/OpenVoiceOS/ovos-utils/tree/V0.0.37a4) (2023-12-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a3...0.0.37a4) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/V0.0.37a3...V0.0.37a4) **Merged pull requests:**