From 09488ffa30872b032a4e29cb5ef5355ef1d46400 Mon Sep 17 00:00:00 2001 From: Steve Penrod Date: Tue, 12 Nov 2019 18:22:52 -0600 Subject: [PATCH 1/3] Add an enclosure service for Picroft The code in ~/enclosure/PicroftEnclosure auto-launches from auto_run.sh and also automatically reloads when the code is changed on disk. Currently it handles the shutdown and reboot messages. Also auto-enter the mycroft environment before jumping to the command line on the additional TTY sessions. --- home/pi/auto_run.sh | 10 +++- home/pi/enclosure/PicroftEnclosure.py | 82 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 home/pi/enclosure/PicroftEnclosure.py diff --git a/home/pi/auto_run.sh b/home/pi/auto_run.sh index 7b600db..3d62be3 100644 --- a/home/pi/auto_run.sh +++ b/home/pi/auto_run.sh @@ -28,7 +28,10 @@ if [ "$SSH_CLIENT" = "" ] && [ "$(/usr/bin/tty)" != "/dev/tty1" ]; then # Quit immediately when running on a local non-primary terminal, - # e.g. when you hit Ctrl+Alt+F2 to open the second term session + # e.g. when you hit Ctrl+Alt+F2 to open the second term session. + + # But go ahead an enter the Mycroft venv... + source mycroft-core/venv-activate.sh -q return 0 fi @@ -905,6 +908,11 @@ then # Launch Mycroft Services ====================== bash "$HOME/mycroft-core/start-mycroft.sh" all + # Start the Picroft enclosure service in the background + cd ~/enclosure + python3 PicroftEnclosure.py >> /var/log/mycroft/enclosure.log 2>&1 & + cd .. + # Display success/welcome message for user echo echo diff --git a/home/pi/enclosure/PicroftEnclosure.py b/home/pi/enclosure/PicroftEnclosure.py new file mode 100644 index 0000000..4ed0a24 --- /dev/null +++ b/home/pi/enclosure/PicroftEnclosure.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +########################################################################## +# PicroftEnclosure.sh +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# This file defines a custom enclosure for your Picroft. Feel free to modify +# it, the automatic update process will not overwrite it. This code can +# monitor the messagebus for system events, listen to GPIOs, or do just about +# anything you'd like. +# +# Changes made to the file will restart the enclosure process automatically +# but be careful -- syntax errors will require manual relaunching or reboot +# after the error is fixed. Relaunch manually via: +# cd ~/enclosure +# python PicroftEnclosure.py + + +from mycroft.client.enclosure.generic import EnclosureGeneric +from time import sleep + +########################################################################## +# Watchdog to reload this script upon modification + +import os, sys +import threading +from os.path import getmtime +WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" +WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] + +def checkForModification(): + for f, mtime in WATCHED_FILES_MTIMES: + if getmtime(f) != mtime: + # Code modification detected, restarting! + os.execv(sys.executable, ['python'] + sys.argv) + threading.Timer(5, checkForModification).start() + +# Kick-off monitor (starts a 5 second timer at the end) +checkForModification() + +########################################################################## + +class EnclosurePicroft(EnclosureGeneric): + + def __init__(self): + super().__init__() + + # Messagebus listeners + self.bus.on("system.shutdown", self.handle_shutdown) + self.bus.on("system.reboot", self.handle_reboot) + + def handle_shutdown(self, message): + os.system("shutdown --poweroff now") + + def handle_reboot(self, message): + # Example of using the self.speak() helper function + self.speak("I'll be right back") + sleep(5) + + os.system("shutdown --reboot now") + + # TODO: Add example GPIO watcher for a contact button? + + # TODO: GUI handler example (e.g. for magic mirror) + + +enc = EnclosurePicroft() +enc.run() + From 01eece6f592f1410328c7395db1b0934d36adc68 Mon Sep 17 00:00:00 2001 From: Steve Penrod Date: Sun, 24 Nov 2019 04:20:55 -0600 Subject: [PATCH 2/3] Revamp of Enclosures Revamped the layout of enclosures on the Picroft image * Code now lives under /opt/mycroft/enclosure instead of ~/enclosure * Setup copies appropriate template to /opt/mycroft/enclosure/my_enclosure.py * A soft link is created to my_enclosure.py in the home directory * Enclosure is run as root * Added basic LED indicator or the Pixel Ring for a Seeed ReSpeaker Mic Array * Add support for a simple button TODO: Enhance Matrix pixel ring support --- home/pi/auto_run.sh | 42 +++- .../mycroft/enclosure/enclosure.py | 0 .../enclosure/templates/AIY_Enclosure.py | 208 +++++++++++++++++ .../enclosure/templates/Matrix_Enclosure.py | 208 +++++++++++++++++ .../templates/ReSpeaker_Enclosure.py | 212 ++++++++++++++++++ opt/mycroft/enclosure/templates/_Enclosure.py | 208 +++++++++++++++++ 6 files changed, 870 insertions(+), 8 deletions(-) rename home/pi/enclosure/PicroftEnclosure.py => opt/mycroft/enclosure/enclosure.py (100%) create mode 100644 opt/mycroft/enclosure/templates/AIY_Enclosure.py create mode 100644 opt/mycroft/enclosure/templates/Matrix_Enclosure.py create mode 100644 opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py create mode 100644 opt/mycroft/enclosure/templates/_Enclosure.py diff --git a/home/pi/auto_run.sh b/home/pi/auto_run.sh index 3d62be3..4f14362 100644 --- a/home/pi/auto_run.sh +++ b/home/pi/auto_run.sh @@ -276,10 +276,16 @@ function setup_wizard() { save_choices fi - - # Check for/download new software (including mycroft-core dependencies, while we are at it). - echo '{"use_branch":"master", "auto_update": true}' > ~/mycroft-core/.dev_opts.json - update_software + if [ -z "$setup_stage" ] ; then + # At the start of setup, install libraries and update to latest Picroft code + + # Install Python library to interact with GPIO + sudo /home/pi/mycroft-core/.venv/bin/pip3 install RPi.GPIO + + # Check for/download new software (including mycroft-core dependencies, while we are at it). + echo '{"use_branch":"master", "auto_update": true}' > ~/mycroft-core/.dev_opts.json + update_software + fi # installs Pulseaudio if not already installed if [ $(dpkg-query -W -f='${Status}' pulseaudio 2>/dev/null | grep -c "ok installed") -eq 0 ]; @@ -288,6 +294,10 @@ function setup_wizard() { fi if [ -z "$audio" ] ; then + # Start with the generic enclosure, create a soft link + cp /opt/mycroft/enclosure/templates/_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py + ln -s /opt/mycroft/enclosure/my_enclosure.py ~/my_enclosure.py + echo echo "=========================================================================" echo "HARDWARE SETUP" @@ -368,6 +378,9 @@ function setup_wizard() { # rebuild venv bash mycroft-core/dev_setup.sh + + # install the AIY-specific enclosure + cp /opt/mycroft/enclosure/templates/AIY_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py echo "Reboot is required, restarting in 5 seconds..." audio="google_aiy" @@ -383,7 +396,7 @@ function setup_wizard() { # Flash latest Seeed firmware echo "Downloading and flashing latest firmware from Seeed..." - sudo /home/pi/mycroft-core/.venv/bin/pip install pyusb click + sudo /home/pi/mycroft-core/.venv/bin/pip3 install pyusb click git clone https://github.com/respeaker/usb_4_mic_array.git cd usb_4_mic_array sudo /home/pi/mycroft-core/.venv/bin/python dfu.py --download 1_channel_firmware.bin @@ -394,6 +407,16 @@ function setup_wizard() { -e "s/aplay -Dhw:0,0 %1/aplay -Dplughw:ArrayUAC10,0 %1/" \ -e "s/mpg123 -a hw:0,0 %1/mpg123 -a plughw:ArrayUAC10,0 %1/" \ /etc/mycroft/mycroft.conf + + # Install the Python libraries to control the ring for the enclosure + cd usb_4_mic_array + git clone https://github.com/respeaker/pixel_ring.git + cd pixel_ring + sudo /home/pi/mycroft-core/.venv/bin/pip3 install setuptools + sudo /home/pi/mycroft-core/.venv/bin/python setup.py install + + # Install the Seeed ReSpeaker-specific enclosure + cp /opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py audio="seed_mic_array_20" break @@ -500,6 +523,9 @@ function setup_wizard() { skip_mic_test=true skip_last_prompt=true mic="matrix_voice" + + # install the Matrix-specific enclosure + cp /opt/mycroft/enclosure/templates/Matrix_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py break ;; 4) @@ -909,9 +935,9 @@ then bash "$HOME/mycroft-core/start-mycroft.sh" all # Start the Picroft enclosure service in the background - cd ~/enclosure - python3 PicroftEnclosure.py >> /var/log/mycroft/enclosure.log 2>&1 & - cd .. + cd /opt/mycroft/enclosure + sudo -- bash -c 'source /home/pi/mycroft-core/venv-activate.sh; python3 my_enclosure.py >> /var/log/mycroft/enclosure.log 2>&1 &' + cd ~ # Display success/welcome message for user echo diff --git a/home/pi/enclosure/PicroftEnclosure.py b/opt/mycroft/enclosure/enclosure.py similarity index 100% rename from home/pi/enclosure/PicroftEnclosure.py rename to opt/mycroft/enclosure/enclosure.py diff --git a/opt/mycroft/enclosure/templates/AIY_Enclosure.py b/opt/mycroft/enclosure/templates/AIY_Enclosure.py new file mode 100644 index 0000000..ac3c9d4 --- /dev/null +++ b/opt/mycroft/enclosure/templates/AIY_Enclosure.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +########################################################################## +# AIY_Enclosure.sh +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# This file defines a simple custom enclosure for your Picroft. By default +# it supports: +# * The AIY button connected as a "Stop" +# * The AIY button's LED as an activity indicator +# * Reboot and shutdown administrative actions +# +# Feel free to modify this code to your own purposes. Changes will not be +# overwritten by the update process will not overwrite it. This code monitors +# the messagebus for system events, listens to GPIOs, and can do just about +# anything you'd like. +# +# Changes made to the file will restart the enclosure process automatically +# but be careful -- syntax errors will require manual relaunching or reboot +# after the error is fixed. Relaunch manually via: +# cd ~/enclosure +# python ~/my_enclosure.py + +from mycroft.client.enclosure.generic import EnclosureGeneric +from time import sleep +import RPi.GPIO as GPIO +import os, sys +import threading +from os.path import getmtime + +########################################################################## +# Watchdog to reload this script upon modification of this file + +WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" +WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] + +def checkForModification(): + for f, mtime in WATCHED_FILES_MTIMES: + if getmtime(f) != mtime: + # Code modification detected, restarting! + os.execv(sys.executable, ['python'] + sys.argv) + threading.Timer(5, checkForModification).start() + +# Kick-off monitor checks every 5 seconds for code changes in this file +checkForModification() + + +########################################################################## + +# BCM GPIO numbers +GPIO_LED = 25 +GPIO_BUTTON = 23 + +class AIY_Enclosure(EnclosureGeneric): + + def __init__(self): + super().__init__() + + # Administrative messages + self.bus.on("system.shutdown", self.on_shutdown) + self.bus.on("system.reboot", self.on_reboot) + self.bus.on("system.update", self.on_software_update) + + # Interaction feedback + self.bus.on("recognizer_loop:wakeword", self.on_wakeword) + self.bus.on("recognizer_loop:record_begin", self.on_record_begin) + self.bus.on("recognizer_loop:record_end", self.on_record_end) + self.bus.on("recognizer_loop:sleep", self.on_sleep) + self.bus.on("recognizer_loop:wake_up", self.on_wake_up) + self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) + self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) + self.bus.on("mycroft.skill.handler.start", self.on_handler_start) + self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) + + # Visual indication that system is booting + self.bus.on("mycroft.skills.initialized", self.on_ready) + + # Setup to support a button on a GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(GPIO_BUTTON, GPIO.IN) + GPIO.add_event_detect(GPIO_BUTTON, GPIO.BOTH, self.on_gpio_button) + + # Setup to support a LED on selected GPIO + GPIO.setup(GPIO_LED, GPIO.OUT) + + self.asleep = False + + def on_ready(self, message): + # Boot has completed, turn off booting visualization + GPIO.output(GPIO_LED, 0) + + def on_sleep(self, message): + # Turn lights orange when asleep + GPIO.output(GPIO_LED, 0) + self.sleep = True + + def on_wake_up(self, message): + GPIO.output(GPIO_LED, 1) + sleep(2) + GPIO.output(GPIO_LED, 0) + self.sleep = False + + ###################################################################### + # Interaction sequence indicators. The trypical sequence is: + # - Wakeword heard + # - Recording begins + # - Recording ends + # - Handler starts + # - Output begins + # - Output ends + # - Handling ends + # There are variations on this, for example an inadvertant recording might never + # begin a handler sequence. Or there might be multiple output begin/end pairs + # within the handler. + + # Illuminate lights when listening + def on_wakeword(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_record_begin(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_record_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + def on_handler_start(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_handler_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + def on_output_start(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_output_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + ###################################################################### + # Simple button support + # + # Wire a button between ground and the GPIO to create a "Stop" button, like + # on a Mycroft Mark 1. The button can also initialize listening without + # speaking the wakeword. + + def on_gpio_button(channel): + if GPIO.input(channel) == GPIO.HIGH: + # Stop Button pressed, similar to the Mark 1 + self.bus.emit(Message("mycroft.stop")) + else: + # Button released + pass + + ###################################################################### + # Administrative actions + # + # Many of the following require root access, but can operate because + # this process is launched using sudo. + + def on_software_update(self, message): + # Mycroft updates itself on reboots + self.speak("Updating system, please wait") + sleep(5) + os.system("cd /home/pi/mycroft-core && git pull") + sleep(2) + + self.speak("Rebooting to apply update") + sleep(5) + os.system("shutdown --reboot now") + + def on_shutdown(self, message): + os.system("shutdown --poweroff now") + + def on_reboot(self, message): + # Example of using the self.speak() helper function + self.speak("I'll be right back") + sleep(5) + os.system("shutdown --reboot now") + + +enc = AIY_Enclosure() +enc.run() diff --git a/opt/mycroft/enclosure/templates/Matrix_Enclosure.py b/opt/mycroft/enclosure/templates/Matrix_Enclosure.py new file mode 100644 index 0000000..12bf09b --- /dev/null +++ b/opt/mycroft/enclosure/templates/Matrix_Enclosure.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +########################################################################## +# Matrix_Enclosure.sh +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# This file defines a simple custom enclosure for your Picroft. By default +# it supports: +# * A GPIO button connected as a "Stop" +# * A GPIO LED as an activity indicator +# * Reboot and shutdown administrative actions +# +# Feel free to modify this code to your own purposes. Changes will not be +# overwritten by the update process will not overwrite it. This code monitors +# the messagebus for system events, listens to GPIOs, and can do just about +# anything you'd like. +# +# Changes made to the file will restart the enclosure process automatically +# but be careful -- syntax errors will require manual relaunching or reboot +# after the error is fixed. Relaunch manually via: +# cd ~/enclosure +# python ~/my_enclosure.py + +from mycroft.client.enclosure.generic import EnclosureGeneric +from time import sleep +import RPi.GPIO as GPIO +import os, sys +import threading +from os.path import getmtime + +########################################################################## +# Watchdog to reload this script upon modification of this file + +WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" +WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] + +def checkForModification(): + for f, mtime in WATCHED_FILES_MTIMES: + if getmtime(f) != mtime: + # Code modification detected, restarting! + os.execv(sys.executable, ['python'] + sys.argv) + threading.Timer(5, checkForModification).start() + +# Kick-off monitor checks every 5 seconds for code changes in this file +checkForModification() + + +########################################################################## + +# BCM GPIO numbers +GPIO_LED = 25 +GPIO_BUTTON = 23 + +class Matrix_Enclosure(EnclosureGeneric): + + def __init__(self): + super().__init__() + + # Administrative messages + self.bus.on("system.shutdown", self.on_shutdown) + self.bus.on("system.reboot", self.on_reboot) + self.bus.on("system.update", self.on_software_update) + + # Interaction feedback + self.bus.on("recognizer_loop:wakeword", self.on_wakeword) + self.bus.on("recognizer_loop:record_begin", self.on_record_begin) + self.bus.on("recognizer_loop:record_end", self.on_record_end) + self.bus.on("recognizer_loop:sleep", self.on_sleep) + self.bus.on("recognizer_loop:wake_up", self.on_wake_up) + self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) + self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) + self.bus.on("mycroft.skill.handler.start", self.on_handler_start) + self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) + + # Visual indication that system is booting + self.bus.on("mycroft.skills.initialized", self.on_ready) + + # Setup to support a button on a GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(GPIO_BUTTON, GPIO.IN) + GPIO.add_event_detect(GPIO_BUTTON, GPIO.BOTH, self.on_gpio_button) + + # Setup to support a LED on selected GPIO + GPIO.setup(GPIO_LED, GPIO.OUT) + + self.asleep = False + + def on_ready(self, message): + # Boot has completed, turn off booting visualization + GPIO.output(GPIO_LED, 0) + + def on_sleep(self, message): + # Turn lights orange when asleep + GPIO.output(GPIO_LED, 0) + self.sleep = True + + def on_wake_up(self, message): + GPIO.output(GPIO_LED, 1) + sleep(2) + GPIO.output(GPIO_LED, 0) + self.sleep = False + + ###################################################################### + # Interaction sequence indicators. The trypical sequence is: + # - Wakeword heard + # - Recording begins + # - Recording ends + # - Handler starts + # - Output begins + # - Output ends + # - Handling ends + # There are variations on this, for example an inadvertant recording might never + # begin a handler sequence. Or there might be multiple output begin/end pairs + # within the handler. + + # Illuminate lights when listening + def on_wakeword(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_record_begin(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_record_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + def on_handler_start(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_handler_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + def on_output_start(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_output_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + ###################################################################### + # Simple button support + # + # Wire a button between ground and the GPIO to create a "Stop" button, like + # on a Mycroft Mark 1. The button can also initialize listening without + # speaking the wakeword. + + def on_gpio_button(channel): + if GPIO.input(channel) == GPIO.HIGH: + # Stop Button pressed, similar to the Mark 1 + self.bus.emit(Message("mycroft.stop")) + else: + # Button released + pass + + ###################################################################### + # Administrative actions + # + # Many of the following require root access, but can operate because + # this process is launched using sudo. + + def on_software_update(self, message): + # Mycroft updates itself on reboots + self.speak("Updating system, please wait") + sleep(5) + os.system("cd /home/pi/mycroft-core && git pull") + sleep(2) + + self.speak("Rebooting to apply update") + sleep(5) + os.system("shutdown --reboot now") + + def on_shutdown(self, message): + os.system("shutdown --poweroff now") + + def on_reboot(self, message): + # Example of using the self.speak() helper function + self.speak("I'll be right back") + sleep(5) + os.system("shutdown --reboot now") + + +enc = Matrix_Enclosure() +enc.run() diff --git a/opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py b/opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py new file mode 100644 index 0000000..99b4bd3 --- /dev/null +++ b/opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python + +########################################################################## +# ReSpeaker_Enclosure.py +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# This file defines a custom enclosure for your Picroft using a Seeed ReSpeaker +# Mic Array. By default it supports: +# * Animations on the pixel ring on the mic array +# * A button connected to the GPIO-17 as a "Stop" +# * Reboot and shutdown administrative actions +# +# Feel free to modify this code to your own purposes. Changes will not be +# overwritten by the update process will not overwrite it. This code monitors +# the messagebus for system events, listens to GPIOs, and can do just about +# anything you'd like. +# +# Changes made to the file will restart the enclosure process automatically +# but be careful -- syntax errors will require manual relaunching or reboot +# after the error is fixed. Relaunch manually via: +# cd ~/enclosure +# python ~/my_enclosure.py + + +from mycroft.client.enclosure.generic import EnclosureGeneric +from time import sleep +import RPi.GPIO as GPIO +import os, sys +import threading +from os.path import getmtime + +# The pixel_ring code is installed from Github +sys.path.insert(1, '/home/pi/usb_4_mic_array/pixel_ring') +from pixel_ring import pixel_ring + +########################################################################## +# Watchdog to reload this script upon modification of this file + +WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" +WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] + +def checkForModification(): + for f, mtime in WATCHED_FILES_MTIMES: + if getmtime(f) != mtime: + # Code modification detected, restarting! + os.execv(sys.executable, ['python'] + sys.argv) + threading.Timer(5, checkForModification).start() + +# Kick-off monitor checks every 5 seconds for code changes in this file +checkForModification() + + +########################################################################## + + +class ReSpeaker_Enclosure(EnclosureGeneric): + + def __init__(self): + super().__init__() + + # Administrative messages + self.bus.on("system.shutdown", self.on_shutdown) + self.bus.on("system.reboot", self.on_reboot) + self.bus.on("system.update", self.on_software_update) + + # Interaction feedback + self.bus.on("recognizer_loop:wakeword", self.on_wakeword) + self.bus.on("recognizer_loop:record_begin", self.on_record_begin) + self.bus.on("recognizer_loop:record_end", self.on_record_end) + self.bus.on("recognizer_loop:sleep", self.on_sleep) + self.bus.on("recognizer_loop:wake_up", self.on_wake_up) + self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) + self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) + self.bus.on("mycroft.skill.handler.start", self.on_handler_start) + self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) + + # Visual indication that system is booting + pixel_ring.set_brightness(2) + pixel_ring.spin() + self.bus.on("mycroft.skills.initialized", self.on_ready) + + # Setup to support a button on GPIO-17 + GPIO.setmode(GPIO.BCM) + GPIO.setup(17,GPIO.IN) + GPIO.add_event_detect(17,GPIO.BOTH, self.on_gpio_button) + + self.asleep = False + + def on_ready(self, message): + # Boot has completed, turn off booting visualization + pixel_ring.off() + + def on_sleep(self, message): + # Turn lights orange when asleep + pixel_ring.set_color(False, 250, 60, 0) + self.sleep = True + + def on_wake_up(self, message): + pixel_ring.spin() + sleep(2) + pixel_ring.off() + self.sleep = False + + ###################################################################### + # Interaction sequence indicators. The trypical sequence is: + # - Wakeword heard + # - Recording begins + # - Recording ends + # - Handler starts + # - Output begins + # - Output ends + # - Handling ends + # There are variations on this, for example an inadvertant recording might never + # begin a handler sequence. Or there might be multiple output begin/end pairs + # within the handler. + + # Illuminate lights when listening + def on_wakeword(self, message): + if self.sleep: + return + pixel_ring.listen() + + def on_record_begin(self, message): + if self.sleep: + return + pixel_ring.listen() + + def on_record_end(self, message): + if self.sleep: + return + pixel_ring.off() + + def on_handler_start(self, message): + if self.sleep: + return + pixel_ring.think() + + def on_handler_end(self, message): + if self.sleep: + return + pixel_ring.off() + + def on_output_start(self, message): + if self.sleep: + return + pixel_ring.speak() + + def on_output_end(self, message): + if self.sleep: + return + pixel_ring.off() + + ###################################################################### + # Simple button support + # + # Wire a button between ground and GPIO-17 to create a "Stop" button, like + # on a Mycroft Mark 1. The button can also initialize listening without + # speaking the wakeword. + + def on_gpio_button(channel): + if GPIO.input(channel) == GPIO.HIGH: + # Stop Button pressed, similar to the Mark 1 + self.bus.emit(Message("mycroft.stop")) + else: + # Button released + pass + + ###################################################################### + # Administrative actions + # + # Many of the following require root access, but can operate because + # this process is launched using sudo. + + def on_software_update(self, message): + # Mycroft updates itself on reboots + self.speak("Updating system, please wait") + sleep(5) + os.system("cd /home/pi/mycroft-core && git pull") + sleep(2) + + self.speak("Rebooting to apply update") + sleep(5) + os.system("shutdown --reboot now") + + def on_shutdown(self, message): + os.system("shutdown --poweroff now") + + def on_reboot(self, message): + # Example of using the self.speak() helper function + self.speak("I'll be right back") + sleep(5) + os.system("shutdown --reboot now") + + +if __name__ == '__main__': + # Running as a script, not a module + enc = ReSpeaker_Enclosure() + enc.run() diff --git a/opt/mycroft/enclosure/templates/_Enclosure.py b/opt/mycroft/enclosure/templates/_Enclosure.py new file mode 100644 index 0000000..950479e --- /dev/null +++ b/opt/mycroft/enclosure/templates/_Enclosure.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +########################################################################## +# _Enclosure.sh +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# This file defines a simple custom enclosure for your Picroft. By default +# it supports: +# * A button connected to the GPIO-23 as a "Stop" +# * A LED connected to the GPIO-21 as an activity indicator +# * Reboot and shutdown administrative actions +# +# Feel free to modify this code to your own purposes. Changes will not be +# overwritten by the update process will not overwrite it. This code monitors +# the messagebus for system events, listens to GPIOs, and can do just about +# anything you'd like. +# +# Changes made to the file will restart the enclosure process automatically +# but be careful -- syntax errors will require manual relaunching or reboot +# after the error is fixed. Relaunch manually via: +# cd ~/enclosure +# python ~/my_enclosure.py + +from mycroft.client.enclosure.generic import EnclosureGeneric +from time import sleep +import RPi.GPIO as GPIO +import os, sys +import threading +from os.path import getmtime + +########################################################################## +# Watchdog to reload this script upon modification of this file + +WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" +WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] + +def checkForModification(): + for f, mtime in WATCHED_FILES_MTIMES: + if getmtime(f) != mtime: + # Code modification detected, restarting! + os.execv(sys.executable, ['python'] + sys.argv) + threading.Timer(5, checkForModification).start() + +# Kick-off monitor checks every 5 seconds for code changes in this file +checkForModification() + + +########################################################################## + +# BCM GPIO numbers +GPIO_LED = 21 +GPIO_BUTTON = 23 + +class Picroft_Enclosure(EnclosureGeneric): + + def __init__(self): + super().__init__() + + # Administrative messages + self.bus.on("system.shutdown", self.on_shutdown) + self.bus.on("system.reboot", self.on_reboot) + self.bus.on("system.update", self.on_software_update) + + # Interaction feedback + self.bus.on("recognizer_loop:wakeword", self.on_wakeword) + self.bus.on("recognizer_loop:record_begin", self.on_record_begin) + self.bus.on("recognizer_loop:record_end", self.on_record_end) + self.bus.on("recognizer_loop:sleep", self.on_sleep) + self.bus.on("recognizer_loop:wake_up", self.on_wake_up) + self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) + self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) + self.bus.on("mycroft.skill.handler.start", self.on_handler_start) + self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) + + # Visual indication that system is booting + self.bus.on("mycroft.skills.initialized", self.on_ready) + + # Setup to support a button on a GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(GPIO_BUTTON, GPIO.IN) + GPIO.add_event_detect(GPIO_BUTTON, GPIO.BOTH, self.on_gpio_button) + + # Setup to support a LED on selected GPIO + GPIO.setup(GPIO_LED, GPIO.OUT) + + self.asleep = False + + def on_ready(self, message): + # Boot has completed, turn off booting visualization + GPIO.output(GPIO_LED, 0) + + def on_sleep(self, message): + # Turn lights orange when asleep + GPIO.output(GPIO_LED, 0) + self.sleep = True + + def on_wake_up(self, message): + GPIO.output(GPIO_LED, 1) + sleep(2) + GPIO.output(GPIO_LED, 0) + self.sleep = False + + ###################################################################### + # Interaction sequence indicators. The trypical sequence is: + # - Wakeword heard + # - Recording begins + # - Recording ends + # - Handler starts + # - Output begins + # - Output ends + # - Handling ends + # There are variations on this, for example an inadvertant recording might never + # begin a handler sequence. Or there might be multiple output begin/end pairs + # within the handler. + + # Illuminate lights when listening + def on_wakeword(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_record_begin(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_record_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + def on_handler_start(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_handler_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + def on_output_start(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 1) + + def on_output_end(self, message): + if self.sleep: + return + GPIO.output(GPIO_LED, 0) + + ###################################################################### + # Simple button support + # + # Wire a button between ground and the GPIO to create a "Stop" button, like + # on a Mycroft Mark 1. The button can also initialize listening without + # speaking the wakeword. + + def on_gpio_button(channel): + if GPIO.input(channel) == GPIO.HIGH: + # Stop Button pressed, similar to the Mark 1 + self.bus.emit(Message("mycroft.stop")) + else: + # Button released + pass + + ###################################################################### + # Administrative actions + # + # Many of the following require root access, but can operate because + # this process is launched using sudo. + + def on_software_update(self, message): + # Mycroft updates itself on reboots + self.speak("Updating system, please wait") + sleep(5) + os.system("cd /home/pi/mycroft-core && git pull") + sleep(2) + + self.speak("Rebooting to apply update") + sleep(5) + os.system("shutdown --reboot now") + + def on_shutdown(self, message): + os.system("shutdown --poweroff now") + + def on_reboot(self, message): + # Example of using the self.speak() helper function + self.speak("I'll be right back") + sleep(5) + os.system("shutdown --reboot now") + + +enc = Picroft_Enclosure() +enc.run() From 083d5c0e501c43d30d70a296cd34d91c03c478f4 Mon Sep 17 00:00:00 2001 From: Steve Penrod Date: Mon, 16 Dec 2019 01:20:05 -0600 Subject: [PATCH 3/3] Minor re-architecting of Picroft enclosures This revamps how code is structured for enclosures. Rather than repeating code in several places, it now uses objects and inheritance. The original attempted to keep all the logic in a single file, but the GPIO objects already added the need for a user to understand object oriented, so this step seems reasonable. The basics heirarchy is: Enclosure Picroft_Enclosure AIY_Enclosure Generic_Enclosure Matrix_Enclosure ReSpeaker_Enclosure The Picroft_Enclosure contains most of the heavy lifting, with just a little in the subclasses to tie to the special cases. The GPIO_Button and GPIO_LED also implement must of the Raspberry Pi behavior. The Button generates actions for Press, Release, Hold and Double Click. The GPIO_LED is a very simple LED which can turn on, off or flash automatically. Also fixes the typo mentioned in Issue #125 --- home/pi/auto_run.sh | 32 +-- opt/mycroft/enclosure/enclosure.py | 82 ------- opt/mycroft/enclosure/launch_enclosure.sh | 22 ++ opt/mycroft/enclosure/lib/__init__.py | 0 opt/mycroft/enclosure/lib/file_watchdog.py | 64 +++++ opt/mycroft/enclosure/lib/gpio_button.py | 114 +++++++++ opt/mycroft/enclosure/lib/gpio_led.py | 77 ++++++ .../enclosure/lib/picroft_enclosure.py | 219 ++++++++++++++++++ .../enclosure/templates/AIY_Enclosure.py | 169 +------------- .../enclosure/templates/Generic_Enclosure.py | 48 ++++ .../enclosure/templates/Matrix_Enclosure.py | 173 +------------- .../templates/ReSpeaker_Enclosure.py | 168 +++----------- opt/mycroft/enclosure/templates/_Enclosure.py | 208 ----------------- 13 files changed, 617 insertions(+), 759 deletions(-) delete mode 100644 opt/mycroft/enclosure/enclosure.py create mode 100644 opt/mycroft/enclosure/launch_enclosure.sh create mode 100644 opt/mycroft/enclosure/lib/__init__.py create mode 100644 opt/mycroft/enclosure/lib/file_watchdog.py create mode 100644 opt/mycroft/enclosure/lib/gpio_button.py create mode 100644 opt/mycroft/enclosure/lib/gpio_led.py create mode 100644 opt/mycroft/enclosure/lib/picroft_enclosure.py create mode 100644 opt/mycroft/enclosure/templates/Generic_Enclosure.py delete mode 100644 opt/mycroft/enclosure/templates/_Enclosure.py diff --git a/home/pi/auto_run.sh b/home/pi/auto_run.sh index 4f14362..ff56585 100644 --- a/home/pi/auto_run.sh +++ b/home/pi/auto_run.sh @@ -29,9 +29,9 @@ if [ "$SSH_CLIENT" = "" ] && [ "$(/usr/bin/tty)" != "/dev/tty1" ]; then # Quit immediately when running on a local non-primary terminal, # e.g. when you hit Ctrl+Alt+F2 to open the second term session. - + # But go ahead an enter the Mycroft venv... - source mycroft-core/venv-activate.sh -q + source mycroft-core/venv-activate.sh -q return 0 fi @@ -113,7 +113,7 @@ function network_setup() { echo " 3) Edit wpa_supplicant.conf directly" echo " 4) Force reboot" echo " 5) Skip network setup for now" - echo -n "${HIGHLIGHT}Choice [1-6]:${RESET} " + echo -n "${HIGHLIGHT}Choice [1-5]:${RESET} " show_prompt=0 fi @@ -278,10 +278,10 @@ function setup_wizard() { if [ -z "$setup_stage" ] ; then # At the start of setup, install libraries and update to latest Picroft code - + # Install Python library to interact with GPIO sudo /home/pi/mycroft-core/.venv/bin/pip3 install RPi.GPIO - + # Check for/download new software (including mycroft-core dependencies, while we are at it). echo '{"use_branch":"master", "auto_update": true}' > ~/mycroft-core/.dev_opts.json update_software @@ -295,9 +295,9 @@ function setup_wizard() { if [ -z "$audio" ] ; then # Start with the generic enclosure, create a soft link - cp /opt/mycroft/enclosure/templates/_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py + cp /opt/mycroft/enclosure/templates/Generic_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py ln -s /opt/mycroft/enclosure/my_enclosure.py ~/my_enclosure.py - + echo echo "=========================================================================" echo "HARDWARE SETUP" @@ -378,7 +378,7 @@ function setup_wizard() { # rebuild venv bash mycroft-core/dev_setup.sh - + # install the AIY-specific enclosure cp /opt/mycroft/enclosure/templates/AIY_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py @@ -407,14 +407,14 @@ function setup_wizard() { -e "s/aplay -Dhw:0,0 %1/aplay -Dplughw:ArrayUAC10,0 %1/" \ -e "s/mpg123 -a hw:0,0 %1/mpg123 -a plughw:ArrayUAC10,0 %1/" \ /etc/mycroft/mycroft.conf - + # Install the Python libraries to control the ring for the enclosure cd usb_4_mic_array git clone https://github.com/respeaker/pixel_ring.git cd pixel_ring sudo /home/pi/mycroft-core/.venv/bin/pip3 install setuptools sudo /home/pi/mycroft-core/.venv/bin/python setup.py install - + # Install the Seeed ReSpeaker-specific enclosure cp /opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py @@ -523,7 +523,7 @@ function setup_wizard() { skip_mic_test=true skip_last_prompt=true mic="matrix_voice" - + # install the Matrix-specific enclosure cp /opt/mycroft/enclosure/templates/Matrix_Enclosure.py /opt/mycroft/enclosure/my_enclosure.py break @@ -784,7 +784,7 @@ then echo "Mycroft quick and easy. Would you like help setting up your system?" echo " Y)es, I'd like the guided setup." echo " N)ope, just get me a command line and get out of my way!" - + # Something in the boot sequence is sending a CR to the screen, so wait # briefly for it to be sent for purely cosmetic purposes. sleep 1 @@ -930,7 +930,7 @@ then # Auto-update to latest version of Picroft scripts and mycroft-core update_software - + # Launch Mycroft Services ====================== bash "$HOME/mycroft-core/start-mycroft.sh" all @@ -944,10 +944,10 @@ then echo mycroft-help echo - - if [ "$initial_setup" = true ]; then + + if [ "$initial_setup" = true ]; then echo "Mycroft is completing startup, ensuring all of the latest versions" - echo "of skills are installed. Within a few minutes you will be prompted" + echo "of skills are installed. Within a few minutes you will be prompted" echo "to pair this device with the required online services at:" echo "https://home.mycroft.ai" echo "where you can enter the pairing code." diff --git a/opt/mycroft/enclosure/enclosure.py b/opt/mycroft/enclosure/enclosure.py deleted file mode 100644 index 4ed0a24..0000000 --- a/opt/mycroft/enclosure/enclosure.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python -########################################################################## -# PicroftEnclosure.sh -# -# Copyright 2019, Stephen Penrod -# -# 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. -########################################################################## - -# This file defines a custom enclosure for your Picroft. Feel free to modify -# it, the automatic update process will not overwrite it. This code can -# monitor the messagebus for system events, listen to GPIOs, or do just about -# anything you'd like. -# -# Changes made to the file will restart the enclosure process automatically -# but be careful -- syntax errors will require manual relaunching or reboot -# after the error is fixed. Relaunch manually via: -# cd ~/enclosure -# python PicroftEnclosure.py - - -from mycroft.client.enclosure.generic import EnclosureGeneric -from time import sleep - -########################################################################## -# Watchdog to reload this script upon modification - -import os, sys -import threading -from os.path import getmtime -WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" -WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] - -def checkForModification(): - for f, mtime in WATCHED_FILES_MTIMES: - if getmtime(f) != mtime: - # Code modification detected, restarting! - os.execv(sys.executable, ['python'] + sys.argv) - threading.Timer(5, checkForModification).start() - -# Kick-off monitor (starts a 5 second timer at the end) -checkForModification() - -########################################################################## - -class EnclosurePicroft(EnclosureGeneric): - - def __init__(self): - super().__init__() - - # Messagebus listeners - self.bus.on("system.shutdown", self.handle_shutdown) - self.bus.on("system.reboot", self.handle_reboot) - - def handle_shutdown(self, message): - os.system("shutdown --poweroff now") - - def handle_reboot(self, message): - # Example of using the self.speak() helper function - self.speak("I'll be right back") - sleep(5) - - os.system("shutdown --reboot now") - - # TODO: Add example GPIO watcher for a contact button? - - # TODO: GUI handler example (e.g. for magic mirror) - - -enc = EnclosurePicroft() -enc.run() - diff --git a/opt/mycroft/enclosure/launch_enclosure.sh b/opt/mycroft/enclosure/launch_enclosure.sh new file mode 100644 index 0000000..bce8045 --- /dev/null +++ b/opt/mycroft/enclosure/launch_enclosure.sh @@ -0,0 +1,22 @@ +#!/bin/bash +########################################################################## +# file_watchdog.py +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# Gain root access (needed for GPIO and Admin actions), load the Mycroft +# virtual environment, and launch the enclosure script +sudo -- bash -c 'source /home/pi/mycroft-core/venv-activate.sh; python3 my_enclosure.py' diff --git a/opt/mycroft/enclosure/lib/__init__.py b/opt/mycroft/enclosure/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opt/mycroft/enclosure/lib/file_watchdog.py b/opt/mycroft/enclosure/lib/file_watchdog.py new file mode 100644 index 0000000..1c7f137 --- /dev/null +++ b/opt/mycroft/enclosure/lib/file_watchdog.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +########################################################################## +# file_watchdog.py +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +########################################################################## +# This implements a watchdog to reload the given script(s) when changes +# are made to the files. In other words, it automatically restarts the given +# program when the program or other watched files are edited and saved to disk. +# +# Usage: +# import lib.file_wachdog as watchdog +# watchdog.watch(__file__) + +import os, sys +import threading +from os.path import getmtime + +__watched_files_mtimes = [] + + +def __checkForModification(): + for f, mtime in __watched_files_mtimes: + if getmtime(f) != mtime: + # Code modification detected, re-launch the main program to + # pick up the changes. + return os.execv(sys.executable, ['python'] + sys.argv) + + # Rerun the check in 5 seconds + t = threading.Timer(5, __checkForModification) + t.daemon = True # don't block the program from ending if main block exits + t.start() + + +def watch(files): + """Start watchdog to relaunch the program when the file(s) change + + Args: + files (string or [string]): File(s) to monitor for changes + """ + global __watched_files_mtimes + + if isinstance(files, list): + __watched_files = files + else: + __watched_files = [files] # support a single filename + __watched_files_mtimes = [(f, getmtime(f)) for f in __watched_files] + + # Kick-off monitor which checks every 5 seconds for code changes + __checkForModification() diff --git a/opt/mycroft/enclosure/lib/gpio_button.py b/opt/mycroft/enclosure/lib/gpio_button.py new file mode 100644 index 0000000..17764b6 --- /dev/null +++ b/opt/mycroft/enclosure/lib/gpio_button.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +########################################################################## +# gpio_button.py +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +import RPi.GPIO as GPIO +from threading import Timer +from time import time + +class GPIO_Button: + + def __init__(self, GPIO_BCM=23, on_press=None, on_release=None, + on_hold=None, on_double_press=None): + """ Interface for a button connected to a GPIO pin + + Respond to a button connected to a GPIO pin (and ground) + + Args: + GPIO_BCM (int, optional): The BCM number of the GPIO pin to use. + Defaults to 23. + """ + self.button_bcm = GPIO_BCM + + self._on_press = on_press + self._on_release = on_release + self._on_hold = on_hold + self._on_double_press = on_double_press + + GPIO.setmode(GPIO.BCM) + GPIO.setup(self.button_bcm, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.add_event_detect(23, GPIO.BOTH, self._on_gpio_button) + self.__timer = None + self.__timer2 = None + self.__last_press = 0 + self.__pressed = False + self.__double_pressed = False + + def __del__(self): + GPIO.remove_event_detect(self.button_bcm) + + def _on_gpio_button(self, channel): + if GPIO.input(channel) == GPIO.HIGH: + # High == button unpressed (pulled high to 3.3v) + self.__pressed = False + if self.__timer: + self.__timer.cancel() + if self.__timer2: + self.__timer2.cancel() + # Wait briefly before firing event to de-bounce + self.__timer = Timer(0.05, self._on_gpio_released) + self.__timer.start() + else: + # Low == button pressed (grounded) + self.__pressed = True + if self.__timer: + self.__timer.cancel() + if self.__timer2: + self.__timer2.cancel() + # Wait briefly before firing event to de-bounce + self.__timer = Timer(0.05, self._on_gpio_pressed) + + # Hold for 1 second to get a "hold" action + self.__timer2 = Timer(1, self._on_gpio_held) + self.__timer.start() + self.__timer2.start() + + def _on_gpio_pressed(self): + self.__timer = None + if self.__pressed: + now = time() + time_since_last = now - self.__last_press + self.__last_press = now + if time_since_last < 1: + # Two presses within a second == double press event + self.__double_pressed = True + if self._on_double_press: + self._on_double_press() + else: + # Single press event + self.__double_pressed = False + if self._on_press: + self._on_press() + + def _on_gpio_released(self): + self.__timer = None + if not self.__pressed and not self.__double_pressed: + if time() - self.__last_press < 1: + # Release event + if self._on_release: + self._on_release() + else: + # Either button was held or a spurrious signal with no press + pass + + def _on_gpio_held(self): + self.__timer2 = None + if self.__pressed: + # Button held event + if self._on_hold: + self._on_hold() diff --git a/opt/mycroft/enclosure/lib/gpio_led.py b/opt/mycroft/enclosure/lib/gpio_led.py new file mode 100644 index 0000000..f6d3a33 --- /dev/null +++ b/opt/mycroft/enclosure/lib/gpio_led.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +########################################################################## +# gpio_led.py +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +import RPi.GPIO as GPIO +from threading import Timer + +class GPIO_LED: + + def __init__(self, GPIO_BCM=21): + """ Interface with an LED connected to a GPIO pin + + Control an LED connected to a GPIO pin (and ground) + + Args: + GPIO_BCM (int, optional): The BCM number of the GPIO pin to use. + Defaults to 21. + """ + super().__init__() + self.led_bcm = GPIO_BCM + GPIO.setmode(GPIO.BCM) + GPIO.setup(self.led_bcm, GPIO.OUT) + self.__timer = None + + def turn_on(self, flash_on_duration=None, flash_off_duration=0.1): + """ Turn on the LED and optionally flash it + + Args: + flash_on_duration (float, optional): Seconds to remain lit, + or None for constant. + flash_off_duration (float, optional): Seconds to go dark. + Defaults to 0.1. + """ + if self.__timer: + self.__timer.cancel() + self.timer = None + GPIO.output(self.led_bcm, GPIO.HIGH) + if flash_on_duration: + self.__timer = Timer(flash_on_duration, self.__flasher, + [True, flash_on_duration, flash_off_duration]) + self.__timer.start() + + def turn_off(self): + """ Illuminate the LED """ + if self.__timer: + self.__timer.cancel() + self.timer = None + GPIO.output(self.led_bcm, GPIO.LOW) + + def __flasher(self, is_on, on_duration, off_duration): + if self.__timer: + self.__timer.cancel() + self.timer = None + if is_on: + GPIO.output(self.led_bcm, GPIO.HIGH) + next_change = off_duration + else: + GPIO.output(self.led_bcm, GPIO.LOW) + next_change = on_duration + self.__timer = Timer(next_change, self.__flasher, + [not is_on, on_duration, off_duration]) + self.__timer.start() diff --git a/opt/mycroft/enclosure/lib/picroft_enclosure.py b/opt/mycroft/enclosure/lib/picroft_enclosure.py new file mode 100644 index 0000000..7d214bb --- /dev/null +++ b/opt/mycroft/enclosure/lib/picroft_enclosure.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python +########################################################################## +# picroft_enclosure.py +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# This file defines the base enclosure for your Picroft. By default +# it supports: +# * A button connected to the GPIO-23 (and ground) as a "Stop" +# * A LED connected to the GPIO-21 as an activity indicator +# * Reboot and shutdown administrative actions +# +# Customizations should be built on top of this. See the ../templates +# folder for examles, and my_enclosure.py which the setup wizard +# creates for your own special behavior. + +from mycroft.client.enclosure.generic import EnclosureGeneric +from mycroft.messagebus.message import Message +from mycroft.util import create_signal +from time import sleep +import os +import sys + +from .gpio_button import GPIO_Button +from .gpio_led import GPIO_LED + +########################################################################## + +class Picroft_Enclosure(EnclosureGeneric): + + def __init__(self, button_gpio_bcm=23, led_gpio_bcm=21): + super().__init__() + + # Administrative messages + self.bus.on("system.shutdown", self.on_shutdown) + self.bus.on("system.reboot", self.on_reboot) + self.bus.on("system.update", self.on_software_update) + + # Interaction feedback + self.bus.on("recognizer_loop:wakeword", self.indicate_listening) + self.bus.on("recognizer_loop:record_begin", self.indicate_listening) + self.bus.on("recognizer_loop:record_end", self.indicate_listening_done) + + self.bus.on("recognizer_loop:sleep", self.indicate_sleeping) + self.bus.on("mycroft.awoken", self.indicate_waking) + + self.bus.on("recognizer_loop:audio_output_start", self.indicate_talking) + self.bus.on("recognizer_loop:audio_output_end", self.indicate_talking_done) + self.bus.on("mycroft.skill.handler.start", self.indicate_thinking) + self.bus.on("mycroft.skill.handler.complete", self.indicate_thinking_done) + + # Visual indication that system is booting + self.bus.on("mycroft.skills.initialized", self.on_ready) + + # Setup to support a button on a GPIO -- default is GPIO23 + if button_gpio_bcm: + self.button = GPIO_Button(GPIO_BCM=button_gpio_bcm, + on_press=self.on_button_pressed, + on_double_press=self.on_button_double_press, + on_release=self.on_button_released, + on_hold=self.on_button_held) + + # Indicate activity using a LED on selected GPIO + if led_gpio_bcm: + self.indicator = GPIO_LED(led_gpio_bcm) # Feedback LED light + else: + self.indicator = None + + self.asleep = False + + self.bus.on("mycroft.skills.initialized", self.indicate_booting_done) + self.indicate_booting() + + def indicate_booting(self): + # Placeholder, override to start something during the bootup sequence + if self.indicator: + self.indicator.turn_on(flash_on_duration=0.5) + + def indicate_booting_done(self, message): + # Boot sequence has completed, turn off booting visualization + # Override if visualization is done differently + if self.indicator: + self.indicator.turn_off() + + def indicate_sleeping(self, message): + # Turn lights when asleep + if self.indicator: + self.indicator.turn_off() + self.asleep = True + + def indicate_waking(self, message): + if self.indicator: + self.indicator.turn_on(flash_on_duration=0.25) + sleep(2) + if self.indicator: + self.indicator.turn_off() + self.asleep = False + + ###################################################################### + # Basic button support. By default behave like the Mark 1 button where + # pressing it either stops a current action or starts listening + # + # Override these actions to react to button events + # NOTE: Basic "button click" actions should happen on the Release instead + # of the Press. This allows the Held to occur without triggering + # a "button click". + + def on_button_pressed(self): + """ The button has been depressed once """ + pass + + def on_button_double_press(self): + """ The button has been depressed within 1 sec of a previous pressing + + NOTE: No on_button_pressed() is generated for the second press. + """ + pass + + def on_button_held(self): + """ The button has been pressed and held for a second """ + pass + + def on_button_released(self): + """ The button has been released after a press + + NOTE: No on_button_released() occurs when held or double-pressed. + """ + # Generate a Listen/Stop signal like the top button press on a Mark 1. + create_signal('buttonPress') + self.bus.emit(Message("mycroft.stop")) + + ###################################################################### + # Interaction sequence indicators. The trypical sequence is: + # - indicate_listening() + # - indicate_listening_done() + # - indicate_thinking() + # - indicate_talking() + # - indicate_talking_done() + # - indicate_thinking_done() + # There are variations on this, for example an inadvertant recording might + # begin a handler, thus no "thinking". Or there might be multiple talking + # sequences within once thinking session. + + # Illuminate lights when listening + def indicate_listening(self, message): + if self.asleep or not self.indicator: + return + self.indicator.turn_on() + + def indicate_listening_done(self, message): + if self.asleep or not self.indicator: + return + self.indicator.turn_off() + + def indicate_thinking(self, message): + if self.asleep or not self.indicator: + return + self.indicator.turn_on() + + def indicate_thinking_done(self, message): + if self.asleep or not self.indicator: + return + self.indicator.turn_off() + + def indicate_talking(self, message): + if self.asleep or not self.indicator: + return + self.indicator.turn_on(flash_on_duration=0.25) + + def indicate_talking_done(self, message): + if self.asleep or not self.indicator: + return + self.indicator.turn_off() + + ###################################################################### + # Administrative actions + # + # Many of the following require root access, but can operate because + # this process is launched using sudo. + + def on_software_update(self, message): + # Mycroft updates itself on reboots + self.speak("Updating system, please wait") + sleep(5) + os.system("cd /home/pi/mycroft-core && git pull") + sleep(2) + + self.speak("Rebooting to apply update") + sleep(5) + if self.indicator: + self.indicator.turn_off() + os.system("shutdown --reboot now") + + def on_shutdown(self, message): + if self.indicator: + self.indicator.turn_off() + os.system("shutdown --poweroff now") + + def on_reboot(self, message): + # Example of using the self.speak() helper function + self.speak("I'll be right back") + sleep(5) + if self.indicator: + self.indicator.turn_off() + os.system("shutdown --reboot now") + diff --git a/opt/mycroft/enclosure/templates/AIY_Enclosure.py b/opt/mycroft/enclosure/templates/AIY_Enclosure.py index ac3c9d4..d78fe96 100644 --- a/opt/mycroft/enclosure/templates/AIY_Enclosure.py +++ b/opt/mycroft/enclosure/templates/AIY_Enclosure.py @@ -34,174 +34,25 @@ # cd ~/enclosure # python ~/my_enclosure.py -from mycroft.client.enclosure.generic import EnclosureGeneric -from time import sleep -import RPi.GPIO as GPIO -import os, sys -import threading -from os.path import getmtime - -########################################################################## -# Watchdog to reload this script upon modification of this file - -WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" -WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] - -def checkForModification(): - for f, mtime in WATCHED_FILES_MTIMES: - if getmtime(f) != mtime: - # Code modification detected, restarting! - os.execv(sys.executable, ['python'] + sys.argv) - threading.Timer(5, checkForModification).start() - -# Kick-off monitor checks every 5 seconds for code changes in this file -checkForModification() +from lib.picroft_enclosure import Picroft_Enclosure +import lib.file_watchdog as watchdog +import lib.GPIO_Button +import lib.GPIO_LED +watchdog.watch(__file__) ########################################################################## -# BCM GPIO numbers -GPIO_LED = 25 -GPIO_BUTTON = 23 - -class AIY_Enclosure(EnclosureGeneric): +class AIY_Enclosure(Picroft_Enclosure): def __init__(self): super().__init__() - # Administrative messages - self.bus.on("system.shutdown", self.on_shutdown) - self.bus.on("system.reboot", self.on_reboot) - self.bus.on("system.update", self.on_software_update) - - # Interaction feedback - self.bus.on("recognizer_loop:wakeword", self.on_wakeword) - self.bus.on("recognizer_loop:record_begin", self.on_record_begin) - self.bus.on("recognizer_loop:record_end", self.on_record_end) - self.bus.on("recognizer_loop:sleep", self.on_sleep) - self.bus.on("recognizer_loop:wake_up", self.on_wake_up) - self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) - self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) - self.bus.on("mycroft.skill.handler.start", self.on_handler_start) - self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) - - # Visual indication that system is booting - self.bus.on("mycroft.skills.initialized", self.on_ready) - - # Setup to support a button on a GPIO - GPIO.setmode(GPIO.BCM) - GPIO.setup(GPIO_BUTTON, GPIO.IN) - GPIO.add_event_detect(GPIO_BUTTON, GPIO.BOTH, self.on_gpio_button) - - # Setup to support a LED on selected GPIO - GPIO.setup(GPIO_LED, GPIO.OUT) - - self.asleep = False - - def on_ready(self, message): - # Boot has completed, turn off booting visualization - GPIO.output(GPIO_LED, 0) - - def on_sleep(self, message): - # Turn lights orange when asleep - GPIO.output(GPIO_LED, 0) - self.sleep = True - - def on_wake_up(self, message): - GPIO.output(GPIO_LED, 1) - sleep(2) - GPIO.output(GPIO_LED, 0) - self.sleep = False - - ###################################################################### - # Interaction sequence indicators. The trypical sequence is: - # - Wakeword heard - # - Recording begins - # - Recording ends - # - Handler starts - # - Output begins - # - Output ends - # - Handling ends - # There are variations on this, for example an inadvertant recording might never - # begin a handler sequence. Or there might be multiple output begin/end pairs - # within the handler. - - # Illuminate lights when listening - def on_wakeword(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_record_begin(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_record_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - def on_handler_start(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_handler_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - def on_output_start(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_output_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - ###################################################################### - # Simple button support - # - # Wire a button between ground and the GPIO to create a "Stop" button, like - # on a Mycroft Mark 1. The button can also initialize listening without - # speaking the wakeword. - - def on_gpio_button(channel): - if GPIO.input(channel) == GPIO.HIGH: - # Stop Button pressed, similar to the Mark 1 - self.bus.emit(Message("mycroft.stop")) - else: - # Button released - pass - - ###################################################################### - # Administrative actions - # - # Many of the following require root access, but can operate because - # this process is launched using sudo. - - def on_software_update(self, message): - # Mycroft updates itself on reboots - self.speak("Updating system, please wait") - sleep(5) - os.system("cd /home/pi/mycroft-core && git pull") - sleep(2) - - self.speak("Rebooting to apply update") - sleep(5) - os.system("shutdown --reboot now") - - def on_shutdown(self, message): - os.system("shutdown --poweroff now") + # Support the standard AIY button + self.stop_button = lib.GPIO_Button(self.bus, GPIO_BCM=23) - def on_reboot(self, message): - # Example of using the self.speak() helper function - self.speak("I'll be right back") - sleep(5) - os.system("shutdown --reboot now") + # Support the standard AIY button's LED as an activity indicator + self.visual = lib.GPIO_LED(GPIO_BCM=25) enc = AIY_Enclosure() diff --git a/opt/mycroft/enclosure/templates/Generic_Enclosure.py b/opt/mycroft/enclosure/templates/Generic_Enclosure.py new file mode 100644 index 0000000..b5cd6bd --- /dev/null +++ b/opt/mycroft/enclosure/templates/Generic_Enclosure.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +########################################################################## +# Generic_Enclosure.py +# +# Copyright 2019, Stephen Penrod +# +# 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. +########################################################################## + +# This file defines a basic enclosure for your Picroft. By default it supports: +# * LED activity light on GPIO-21 +# * A button connected to the GPIO-23 (and ground) as a Stop button +# * Administrative actions such as reboot and shutdown +# +# Feel free to modify this code to your own purposes. Changes will not be +# overwritten by the update process will not change it. This code monitors +# the messagebus for system events, listens to GPIOs, and can do just about +# anything you'd like. +# +# Changes to my_enclosure.py will restart the enclosure process automatically +# but be careful -- syntax errors will require manual relaunching or reboot +# after the error is fixed. + +from lib.picroft_enclosure import Picroft_Enclosure +from time import sleep + +import lib.file_watchdog as watchdog +watchdog.watch(__file__) + +########################################################################## + +class Generic_Enclosure(Picroft_Enclosure): + + def __init__(self): + super().__init__(button_gpio_bcm=23, led_gpio_bcm=21) + +enc = Generic_Enclosure() +enc.run() diff --git a/opt/mycroft/enclosure/templates/Matrix_Enclosure.py b/opt/mycroft/enclosure/templates/Matrix_Enclosure.py index 12bf09b..25622bc 100644 --- a/opt/mycroft/enclosure/templates/Matrix_Enclosure.py +++ b/opt/mycroft/enclosure/templates/Matrix_Enclosure.py @@ -19,12 +19,12 @@ # This file defines a simple custom enclosure for your Picroft. By default # it supports: -# * A GPIO button connected as a "Stop" -# * A GPIO LED as an activity indicator -# * Reboot and shutdown administrative actions +# * A button connected to the GPIO-23 (and ground) as a Stop button +# * LED activity light on GPIO-21 +# * Administrative actions such as reboot and shutdown # # Feel free to modify this code to your own purposes. Changes will not be -# overwritten by the update process will not overwrite it. This code monitors +# overwritten by the update process will not change it. This code monitors # the messagebus for system events, listens to GPIOs, and can do just about # anything you'd like. # @@ -34,174 +34,19 @@ # cd ~/enclosure # python ~/my_enclosure.py -from mycroft.client.enclosure.generic import EnclosureGeneric -from time import sleep -import RPi.GPIO as GPIO -import os, sys -import threading -from os.path import getmtime - -########################################################################## -# Watchdog to reload this script upon modification of this file - -WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" -WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] - -def checkForModification(): - for f, mtime in WATCHED_FILES_MTIMES: - if getmtime(f) != mtime: - # Code modification detected, restarting! - os.execv(sys.executable, ['python'] + sys.argv) - threading.Timer(5, checkForModification).start() - -# Kick-off monitor checks every 5 seconds for code changes in this file -checkForModification() +from lib.picroft_enclosure import Picroft_Enclosure +import lib.file_watchdog as watchdog +watchdog.watch(__file__) ########################################################################## -# BCM GPIO numbers -GPIO_LED = 25 -GPIO_BUTTON = 23 - -class Matrix_Enclosure(EnclosureGeneric): +class Matrix_Enclosure(Picroft_Enclosure): def __init__(self): super().__init__() - # Administrative messages - self.bus.on("system.shutdown", self.on_shutdown) - self.bus.on("system.reboot", self.on_reboot) - self.bus.on("system.update", self.on_software_update) - - # Interaction feedback - self.bus.on("recognizer_loop:wakeword", self.on_wakeword) - self.bus.on("recognizer_loop:record_begin", self.on_record_begin) - self.bus.on("recognizer_loop:record_end", self.on_record_end) - self.bus.on("recognizer_loop:sleep", self.on_sleep) - self.bus.on("recognizer_loop:wake_up", self.on_wake_up) - self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) - self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) - self.bus.on("mycroft.skill.handler.start", self.on_handler_start) - self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) - - # Visual indication that system is booting - self.bus.on("mycroft.skills.initialized", self.on_ready) - - # Setup to support a button on a GPIO - GPIO.setmode(GPIO.BCM) - GPIO.setup(GPIO_BUTTON, GPIO.IN) - GPIO.add_event_detect(GPIO_BUTTON, GPIO.BOTH, self.on_gpio_button) - - # Setup to support a LED on selected GPIO - GPIO.setup(GPIO_LED, GPIO.OUT) - - self.asleep = False - - def on_ready(self, message): - # Boot has completed, turn off booting visualization - GPIO.output(GPIO_LED, 0) - - def on_sleep(self, message): - # Turn lights orange when asleep - GPIO.output(GPIO_LED, 0) - self.sleep = True - - def on_wake_up(self, message): - GPIO.output(GPIO_LED, 1) - sleep(2) - GPIO.output(GPIO_LED, 0) - self.sleep = False - - ###################################################################### - # Interaction sequence indicators. The trypical sequence is: - # - Wakeword heard - # - Recording begins - # - Recording ends - # - Handler starts - # - Output begins - # - Output ends - # - Handling ends - # There are variations on this, for example an inadvertant recording might never - # begin a handler sequence. Or there might be multiple output begin/end pairs - # within the handler. - - # Illuminate lights when listening - def on_wakeword(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_record_begin(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_record_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - def on_handler_start(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_handler_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - def on_output_start(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_output_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - ###################################################################### - # Simple button support - # - # Wire a button between ground and the GPIO to create a "Stop" button, like - # on a Mycroft Mark 1. The button can also initialize listening without - # speaking the wakeword. - - def on_gpio_button(channel): - if GPIO.input(channel) == GPIO.HIGH: - # Stop Button pressed, similar to the Mark 1 - self.bus.emit(Message("mycroft.stop")) - else: - # Button released - pass - - ###################################################################### - # Administrative actions - # - # Many of the following require root access, but can operate because - # this process is launched using sudo. - - def on_software_update(self, message): - # Mycroft updates itself on reboots - self.speak("Updating system, please wait") - sleep(5) - os.system("cd /home/pi/mycroft-core && git pull") - sleep(2) - - self.speak("Rebooting to apply update") - sleep(5) - os.system("shutdown --reboot now") - - def on_shutdown(self, message): - os.system("shutdown --poweroff now") - - def on_reboot(self, message): - # Example of using the self.speak() helper function - self.speak("I'll be right back") - sleep(5) - os.system("shutdown --reboot now") + # TODO: Use the Matrix light array to indicate status enc = Matrix_Enclosure() diff --git a/opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py b/opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py index 99b4bd3..3ea3687 100644 --- a/opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py +++ b/opt/mycroft/enclosure/templates/ReSpeaker_Enclosure.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - ########################################################################## # ReSpeaker_Enclosure.py # @@ -21,7 +20,7 @@ # This file defines a custom enclosure for your Picroft using a Seeed ReSpeaker # Mic Array. By default it supports: # * Animations on the pixel ring on the mic array -# * A button connected to the GPIO-17 as a "Stop" +# * A button connected to the GPIO-23 as a Stop button # * Reboot and shutdown administrative actions # # Feel free to modify this code to your own purposes. Changes will not be @@ -29,184 +28,93 @@ # the messagebus for system events, listens to GPIOs, and can do just about # anything you'd like. # -# Changes made to the file will restart the enclosure process automatically +# Changes to my_enclosure.py will restart the enclosure process automatically # but be careful -- syntax errors will require manual relaunching or reboot -# after the error is fixed. Relaunch manually via: -# cd ~/enclosure -# python ~/my_enclosure.py - +# after the error is fixed. -from mycroft.client.enclosure.generic import EnclosureGeneric +from lib.picroft_enclosure import Picroft_Enclosure from time import sleep -import RPi.GPIO as GPIO -import os, sys -import threading -from os.path import getmtime -# The pixel_ring code is installed from Github +# The pixel_ring code is installed from Github in the home directory +import sys sys.path.insert(1, '/home/pi/usb_4_mic_array/pixel_ring') from pixel_ring import pixel_ring -########################################################################## -# Watchdog to reload this script upon modification of this file - -WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" -WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] - -def checkForModification(): - for f, mtime in WATCHED_FILES_MTIMES: - if getmtime(f) != mtime: - # Code modification detected, restarting! - os.execv(sys.executable, ['python'] + sys.argv) - threading.Timer(5, checkForModification).start() - -# Kick-off monitor checks every 5 seconds for code changes in this file -checkForModification() - +import lib.file_watchdog as watchdog +watchdog.watch(__file__) ########################################################################## - -class ReSpeaker_Enclosure(EnclosureGeneric): +class ReSpeaker_Enclosure(Picroft_Enclosure): def __init__(self): - super().__init__() - - # Administrative messages - self.bus.on("system.shutdown", self.on_shutdown) - self.bus.on("system.reboot", self.on_reboot) - self.bus.on("system.update", self.on_software_update) - - # Interaction feedback - self.bus.on("recognizer_loop:wakeword", self.on_wakeword) - self.bus.on("recognizer_loop:record_begin", self.on_record_begin) - self.bus.on("recognizer_loop:record_end", self.on_record_end) - self.bus.on("recognizer_loop:sleep", self.on_sleep) - self.bus.on("recognizer_loop:wake_up", self.on_wake_up) - self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) - self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) - self.bus.on("mycroft.skill.handler.start", self.on_handler_start) - self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) + super().__init__(led_gpio_bcm=None) # Pixel Ring is used to show state + # instead of a LED on a GPIO + def indicate_booting(self): # Visual indication that system is booting pixel_ring.set_brightness(2) pixel_ring.spin() - self.bus.on("mycroft.skills.initialized", self.on_ready) - - # Setup to support a button on GPIO-17 - GPIO.setmode(GPIO.BCM) - GPIO.setup(17,GPIO.IN) - GPIO.add_event_detect(17,GPIO.BOTH, self.on_gpio_button) - self.asleep = False - - def on_ready(self, message): + def indicate_booting_done(self, message): # Boot has completed, turn off booting visualization pixel_ring.off() - def on_sleep(self, message): + def indicate_sleeping(self, message): # Turn lights orange when asleep pixel_ring.set_color(False, 250, 60, 0) - self.sleep = True + self.asleep = True - def on_wake_up(self, message): + def indicate_waking(self, message): + # Restore lights when woken pixel_ring.spin() sleep(2) pixel_ring.off() - self.sleep = False + self.asleep = False ###################################################################### # Interaction sequence indicators. The trypical sequence is: - # - Wakeword heard - # - Recording begins - # - Recording ends - # - Handler starts - # - Output begins - # - Output ends - # - Handling ends + # - indicate_listening() + # - indicate_listening_done() + # - indicate_thinking() + # - indicate_talking() + # - indicate_talking_done() + # - indicate_thinking_done() # There are variations on this, for example an inadvertant recording might never # begin a handler sequence. Or there might be multiple output begin/end pairs # within the handler. # Illuminate lights when listening - def on_wakeword(self, message): - if self.sleep: + def indicate_listening(self, message): + if self.asleep: return pixel_ring.listen() - def on_record_begin(self, message): - if self.sleep: - return - pixel_ring.listen() - - def on_record_end(self, message): - if self.sleep: + def indicate_listening_done(self, message): + if self.asleep: return pixel_ring.off() - def on_handler_start(self, message): - if self.sleep: + def indicate_thinking(self, message): + if self.asleep: return pixel_ring.think() - def on_handler_end(self, message): - if self.sleep: + def indicate_thinking_done(self, message): + if self.asleep: return pixel_ring.off() - def on_output_start(self, message): - if self.sleep: + def indicate_talking(self, message): + if self.asleep: return pixel_ring.speak() - def on_output_end(self, message): - if self.sleep: + def indicate_talking_done(self, message): + if self.asleep: return pixel_ring.off() - ###################################################################### - # Simple button support - # - # Wire a button between ground and GPIO-17 to create a "Stop" button, like - # on a Mycroft Mark 1. The button can also initialize listening without - # speaking the wakeword. - - def on_gpio_button(channel): - if GPIO.input(channel) == GPIO.HIGH: - # Stop Button pressed, similar to the Mark 1 - self.bus.emit(Message("mycroft.stop")) - else: - # Button released - pass - - ###################################################################### - # Administrative actions - # - # Many of the following require root access, but can operate because - # this process is launched using sudo. - - def on_software_update(self, message): - # Mycroft updates itself on reboots - self.speak("Updating system, please wait") - sleep(5) - os.system("cd /home/pi/mycroft-core && git pull") - sleep(2) - - self.speak("Rebooting to apply update") - sleep(5) - os.system("shutdown --reboot now") - - def on_shutdown(self, message): - os.system("shutdown --poweroff now") - - def on_reboot(self, message): - # Example of using the self.speak() helper function - self.speak("I'll be right back") - sleep(5) - os.system("shutdown --reboot now") - -if __name__ == '__main__': - # Running as a script, not a module - enc = ReSpeaker_Enclosure() - enc.run() +enc = ReSpeaker_Enclosure() +enc.run() diff --git a/opt/mycroft/enclosure/templates/_Enclosure.py b/opt/mycroft/enclosure/templates/_Enclosure.py deleted file mode 100644 index 950479e..0000000 --- a/opt/mycroft/enclosure/templates/_Enclosure.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python -########################################################################## -# _Enclosure.sh -# -# Copyright 2019, Stephen Penrod -# -# 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. -########################################################################## - -# This file defines a simple custom enclosure for your Picroft. By default -# it supports: -# * A button connected to the GPIO-23 as a "Stop" -# * A LED connected to the GPIO-21 as an activity indicator -# * Reboot and shutdown administrative actions -# -# Feel free to modify this code to your own purposes. Changes will not be -# overwritten by the update process will not overwrite it. This code monitors -# the messagebus for system events, listens to GPIOs, and can do just about -# anything you'd like. -# -# Changes made to the file will restart the enclosure process automatically -# but be careful -- syntax errors will require manual relaunching or reboot -# after the error is fixed. Relaunch manually via: -# cd ~/enclosure -# python ~/my_enclosure.py - -from mycroft.client.enclosure.generic import EnclosureGeneric -from time import sleep -import RPi.GPIO as GPIO -import os, sys -import threading -from os.path import getmtime - -########################################################################## -# Watchdog to reload this script upon modification of this file - -WATCHED_FILES = [__file__] # Add other dependencies as desired, e.g. "my.json" -WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] - -def checkForModification(): - for f, mtime in WATCHED_FILES_MTIMES: - if getmtime(f) != mtime: - # Code modification detected, restarting! - os.execv(sys.executable, ['python'] + sys.argv) - threading.Timer(5, checkForModification).start() - -# Kick-off monitor checks every 5 seconds for code changes in this file -checkForModification() - - -########################################################################## - -# BCM GPIO numbers -GPIO_LED = 21 -GPIO_BUTTON = 23 - -class Picroft_Enclosure(EnclosureGeneric): - - def __init__(self): - super().__init__() - - # Administrative messages - self.bus.on("system.shutdown", self.on_shutdown) - self.bus.on("system.reboot", self.on_reboot) - self.bus.on("system.update", self.on_software_update) - - # Interaction feedback - self.bus.on("recognizer_loop:wakeword", self.on_wakeword) - self.bus.on("recognizer_loop:record_begin", self.on_record_begin) - self.bus.on("recognizer_loop:record_end", self.on_record_end) - self.bus.on("recognizer_loop:sleep", self.on_sleep) - self.bus.on("recognizer_loop:wake_up", self.on_wake_up) - self.bus.on("recognizer_loop:audio_output_start", self.on_output_start) - self.bus.on("recognizer_loop:audio_output_end", self.on_output_end) - self.bus.on("mycroft.skill.handler.start", self.on_handler_start) - self.bus.on("mycroft.skill.handler.complete", self.on_handler_end) - - # Visual indication that system is booting - self.bus.on("mycroft.skills.initialized", self.on_ready) - - # Setup to support a button on a GPIO - GPIO.setmode(GPIO.BCM) - GPIO.setup(GPIO_BUTTON, GPIO.IN) - GPIO.add_event_detect(GPIO_BUTTON, GPIO.BOTH, self.on_gpio_button) - - # Setup to support a LED on selected GPIO - GPIO.setup(GPIO_LED, GPIO.OUT) - - self.asleep = False - - def on_ready(self, message): - # Boot has completed, turn off booting visualization - GPIO.output(GPIO_LED, 0) - - def on_sleep(self, message): - # Turn lights orange when asleep - GPIO.output(GPIO_LED, 0) - self.sleep = True - - def on_wake_up(self, message): - GPIO.output(GPIO_LED, 1) - sleep(2) - GPIO.output(GPIO_LED, 0) - self.sleep = False - - ###################################################################### - # Interaction sequence indicators. The trypical sequence is: - # - Wakeword heard - # - Recording begins - # - Recording ends - # - Handler starts - # - Output begins - # - Output ends - # - Handling ends - # There are variations on this, for example an inadvertant recording might never - # begin a handler sequence. Or there might be multiple output begin/end pairs - # within the handler. - - # Illuminate lights when listening - def on_wakeword(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_record_begin(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_record_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - def on_handler_start(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_handler_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - def on_output_start(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 1) - - def on_output_end(self, message): - if self.sleep: - return - GPIO.output(GPIO_LED, 0) - - ###################################################################### - # Simple button support - # - # Wire a button between ground and the GPIO to create a "Stop" button, like - # on a Mycroft Mark 1. The button can also initialize listening without - # speaking the wakeword. - - def on_gpio_button(channel): - if GPIO.input(channel) == GPIO.HIGH: - # Stop Button pressed, similar to the Mark 1 - self.bus.emit(Message("mycroft.stop")) - else: - # Button released - pass - - ###################################################################### - # Administrative actions - # - # Many of the following require root access, but can operate because - # this process is launched using sudo. - - def on_software_update(self, message): - # Mycroft updates itself on reboots - self.speak("Updating system, please wait") - sleep(5) - os.system("cd /home/pi/mycroft-core && git pull") - sleep(2) - - self.speak("Rebooting to apply update") - sleep(5) - os.system("shutdown --reboot now") - - def on_shutdown(self, message): - os.system("shutdown --poweroff now") - - def on_reboot(self, message): - # Example of using the self.speak() helper function - self.speak("I'll be right back") - sleep(5) - os.system("shutdown --reboot now") - - -enc = Picroft_Enclosure() -enc.run()