├── alexa.py ├── README.md ├── google_assistant.py ├── pixels.py └── apa102.py /alexa.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hands-free Voice Assistant with Snowboy and Alexa Voice Service. The wake-up keyword is "alexa" 3 | 4 | Requirement: 5 | pip install avs 6 | pip install voice-engine 7 | """ 8 | 9 | 10 | import time 11 | import logging 12 | from voice_engine.source import Source 13 | from voice_engine.kws import KWS 14 | from avs.alexa import Alexa 15 | from pixels import pixels 16 | 17 | 18 | def main(): 19 | logging.basicConfig(level=logging.DEBUG) 20 | 21 | src = Source(rate=16000, frames_size=320) 22 | kws = KWS(model='alexa', sensitivity=0.8) 23 | alexa = Alexa() 24 | 25 | 26 | alexa.state_listener.on_listening = pixels.listen 27 | alexa.state_listener.on_thinking = pixels.think 28 | alexa.state_listener.on_speaking = pixels.speak 29 | alexa.state_listener.on_finished = pixels.off 30 | 31 | 32 | src.link(kws) 33 | kws.link(alexa) 34 | 35 | def on_detected(keyword): 36 | logging.info('detected {}'.format(keyword)) 37 | alexa.listen() 38 | 39 | kws.set_callback(on_detected) 40 | 41 | src.recursive_start() 42 | 43 | while True: 44 | try: 45 | time.sleep(1) 46 | except KeyboardInterrupt: 47 | break 48 | 49 | src.recursive_stop() 50 | 51 | 52 | if __name__ == '__main__': 53 | main() 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MIC HAT for Raspberry Pi 2 | ======================== 3 | 4 | To build voice enabled projects with Google Assistant, Amazon Alexa Voice service and etc. 5 | 6 | [![](https://github.com/SeeedDocument/MIC_HATv1.0_for_raspberrypi/blob/master/img/mic_hatv1.0.png?raw=true)](https://www.seeedstudio.com/ReSpeaker-2-Mics-Pi-HAT-p-2874.html) 7 | 8 | 9 | ## Requirements 10 | + [seeed-voicecard](https://github.com/respeaker/seeed-voicecard), the kernel driver for on-board WM8960 codec 11 | + [spidev](https://pypi.python.org/pypi/spidev) for on-board SPI interface APA102 LEDs 12 | + [google-assistant-library](https://github.com/googlesamples/assistant-sdk-python/tree/master/google-assistant-sdk/googlesamples/assistant/library) 13 | + [avs](https://github.com/respeaker/avs), Alexa Voice Service client python library 14 | + [voice-engine](https://github.com/voice-engine/voice-engine) 15 | 16 | ## Setup 17 | 1. Go to [seeed-voicecard](https://github.com/respeaker/seeed-voicecard) and install it 18 | 2. Use `raspi-config` to enable SPI. 19 | 3. Install `spidev` (`pip install spidev`). 20 | 4. Run `python pixels.py` to test the pixels. 21 | 22 | ## Build a Google Home like device with Google Assistant SDK 23 | 1. Setup [google-assistant-library](https://github.com/googlesamples/assistant-sdk-python/tree/master/google-assistant-sdk/googlesamples/assistant/library) 24 | 2. Run `python google_assistant.py` 25 | 26 | ## Build an Echo like device with Alexa Voice Service 27 | 1. `pip install avs voice-engine` 28 | 2. Go to [snowboy](https://github.com/Kitt-AI/snowboy) and install its python binding. 29 | 3. `python alexa.py` 30 | -------------------------------------------------------------------------------- /google_assistant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import print_function 19 | 20 | import argparse 21 | import os.path 22 | import json 23 | 24 | import google.oauth2.credentials 25 | 26 | from google.assistant.library import Assistant 27 | from google.assistant.library.event import EventType 28 | from google.assistant.library.file_helpers import existing_file 29 | from pixels import pixels 30 | 31 | 32 | def process_event(event): 33 | """Pretty prints events. 34 | 35 | Prints all events that occur with two spaces between each new 36 | conversation and a single space between turns of a conversation. 37 | 38 | Args: 39 | event(event.Event): The current event to process. 40 | """ 41 | if event.type == EventType.ON_CONVERSATION_TURN_STARTED: 42 | print() 43 | pixels.wakeup() 44 | 45 | print(event) 46 | 47 | if event.type == EventType.ON_END_OF_UTTERANCE: 48 | pixels.think() 49 | 50 | if event.type == EventType.ON_RESPONDING_STARTED: 51 | pixels.speak() 52 | 53 | if event.type == EventType.ON_CONVERSATION_TURN_FINISHED: 54 | pixels.off() 55 | if event.args and event.args['with_follow_on_turn']: 56 | pixels.listen() 57 | 58 | 59 | def main(): 60 | parser = argparse.ArgumentParser( 61 | formatter_class=argparse.RawTextHelpFormatter) 62 | parser.add_argument('--credentials', type=existing_file, 63 | metavar='OAUTH2_CREDENTIALS_FILE', 64 | default=os.path.join( 65 | os.path.expanduser('~/.config'), 66 | 'google-oauthlib-tool', 67 | 'credentials.json' 68 | ), 69 | help='Path to store and read OAuth2 credentials') 70 | args = parser.parse_args() 71 | with open(args.credentials, 'r') as f: 72 | credentials = google.oauth2.credentials.Credentials(token=None, 73 | **json.load(f)) 74 | 75 | with Assistant(credentials) as assistant: 76 | for event in assistant.start(): 77 | process_event(event) 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /pixels.py: -------------------------------------------------------------------------------- 1 | 2 | import apa102 3 | import time 4 | import threading 5 | try: 6 | import queue as Queue 7 | except ImportError: 8 | import Queue as Queue 9 | 10 | 11 | class Pixels: 12 | PIXELS_N = 3 13 | 14 | def __init__(self): 15 | self.basis = [0] * 3 * self.PIXELS_N 16 | self.basis[0] = 1 17 | self.basis[4] = 1 18 | self.basis[8] = 2 19 | 20 | self.colors = [0] * 3 * self.PIXELS_N 21 | self.dev = apa102.APA102(num_led=self.PIXELS_N) 22 | 23 | self.next = threading.Event() 24 | self.queue = Queue.Queue() 25 | self.thread = threading.Thread(target=self._run) 26 | self.thread.daemon = True 27 | self.thread.start() 28 | 29 | def wakeup(self, direction=0): 30 | def f(): 31 | self._wakeup(direction) 32 | 33 | self.next.set() 34 | self.queue.put(f) 35 | 36 | def listen(self): 37 | self.next.set() 38 | self.queue.put(self._listen) 39 | 40 | def think(self): 41 | self.next.set() 42 | self.queue.put(self._think) 43 | 44 | def speak(self): 45 | self.next.set() 46 | self.queue.put(self._speak) 47 | 48 | def off(self): 49 | self.next.set() 50 | self.queue.put(self._off) 51 | 52 | def _run(self): 53 | while True: 54 | func = self.queue.get() 55 | func() 56 | 57 | def _wakeup(self, direction=0): 58 | for i in range(1, 25): 59 | colors = [i * v for v in self.basis] 60 | self.write(colors) 61 | time.sleep(0.01) 62 | 63 | self.colors = colors 64 | 65 | def _listen(self): 66 | for i in range(1, 25): 67 | colors = [i * v for v in self.basis] 68 | self.write(colors) 69 | time.sleep(0.01) 70 | 71 | self.colors = colors 72 | 73 | def _think(self): 74 | colors = self.colors 75 | 76 | self.next.clear() 77 | while not self.next.is_set(): 78 | colors = colors[3:] + colors[:3] 79 | self.write(colors) 80 | time.sleep(0.2) 81 | 82 | t = 0.1 83 | for i in range(0, 5): 84 | colors = colors[3:] + colors[:3] 85 | self.write([(v * (4 - i) / 4) for v in colors]) 86 | time.sleep(t) 87 | t /= 2 88 | 89 | # time.sleep(0.5) 90 | 91 | self.colors = colors 92 | 93 | def _speak(self): 94 | colors = self.colors 95 | 96 | self.next.clear() 97 | while not self.next.is_set(): 98 | for i in range(5, 25): 99 | self.write([(v * i / 24) for v in colors]) 100 | time.sleep(0.01) 101 | 102 | time.sleep(0.3) 103 | 104 | for i in range(24, 4, -1): 105 | self.write([(v * i / 24) for v in colors]) 106 | time.sleep(0.01) 107 | 108 | time.sleep(0.3) 109 | 110 | self._off() 111 | 112 | def _off(self): 113 | self.write([0] * 3 * self.PIXELS_N) 114 | 115 | def write(self, colors): 116 | for i in range(self.PIXELS_N): 117 | self.dev.set_pixel(i, int(colors[3*i]), int(colors[3*i + 1]), int(colors[3*i + 2])) 118 | 119 | self.dev.show() 120 | 121 | 122 | pixels = Pixels() 123 | 124 | 125 | if __name__ == '__main__': 126 | while True: 127 | 128 | try: 129 | pixels.wakeup() 130 | time.sleep(3) 131 | pixels.think() 132 | time.sleep(3) 133 | pixels.speak() 134 | time.sleep(3) 135 | pixels.off() 136 | time.sleep(3) 137 | except KeyboardInterrupt: 138 | break 139 | 140 | 141 | pixels.off() 142 | time.sleep(1) 143 | -------------------------------------------------------------------------------- /apa102.py: -------------------------------------------------------------------------------- 1 | """ 2 | from https://github.com/tinue/APA102_Pi 3 | This is the main driver module for APA102 LEDs 4 | """ 5 | import spidev 6 | from math import ceil 7 | 8 | RGB_MAP = { 'rgb': [3, 2, 1], 'rbg': [3, 1, 2], 'grb': [2, 3, 1], 9 | 'gbr': [2, 1, 3], 'brg': [1, 3, 2], 'bgr': [1, 2, 3] } 10 | 11 | class APA102: 12 | """ 13 | Driver for APA102 LEDS (aka "DotStar"). 14 | 15 | (c) Martin Erzberger 2016-2017 16 | 17 | My very first Python code, so I am sure there is a lot to be optimized ;) 18 | 19 | Public methods are: 20 | - set_pixel 21 | - set_pixel_rgb 22 | - show 23 | - clear_strip 24 | - cleanup 25 | 26 | Helper methods for color manipulation are: 27 | - combine_color 28 | - wheel 29 | 30 | The rest of the methods are used internally and should not be used by the 31 | user of the library. 32 | 33 | Very brief overview of APA102: An APA102 LED is addressed with SPI. The bits 34 | are shifted in one by one, starting with the least significant bit. 35 | 36 | An LED usually just forwards everything that is sent to its data-in to 37 | data-out. While doing this, it remembers its own color and keeps glowing 38 | with that color as long as there is power. 39 | 40 | An LED can be switched to not forward the data, but instead use the data 41 | to change it's own color. This is done by sending (at least) 32 bits of 42 | zeroes to data-in. The LED then accepts the next correct 32 bit LED 43 | frame (with color information) as its new color setting. 44 | 45 | After having received the 32 bit color frame, the LED changes color, 46 | and then resumes to just copying data-in to data-out. 47 | 48 | The really clever bit is this: While receiving the 32 bit LED frame, 49 | the LED sends zeroes on its data-out line. Because a color frame is 50 | 32 bits, the LED sends 32 bits of zeroes to the next LED. 51 | As we have seen above, this means that the next LED is now ready 52 | to accept a color frame and update its color. 53 | 54 | So that's really the entire protocol: 55 | - Start by sending 32 bits of zeroes. This prepares LED 1 to update 56 | its color. 57 | - Send color information one by one, starting with the color for LED 1, 58 | then LED 2 etc. 59 | - Finish off by cycling the clock line a few times to get all data 60 | to the very last LED on the strip 61 | 62 | The last step is necessary, because each LED delays forwarding the data 63 | a bit. Imagine ten people in a row. When you yell the last color 64 | information, i.e. the one for person ten, to the first person in 65 | the line, then you are not finished yet. Person one has to turn around 66 | and yell it to person 2, and so on. So it takes ten additional "dummy" 67 | cycles until person ten knows the color. When you look closer, 68 | you will see that not even person 9 knows its own color yet. This 69 | information is still with person 2. Essentially the driver sends additional 70 | zeroes to LED 1 as long as it takes for the last color frame to make it 71 | down the line to the last LED. 72 | """ 73 | # Constants 74 | MAX_BRIGHTNESS = 31 # Safeguard: Set to a value appropriate for your setup 75 | LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits 76 | 77 | def __init__(self, num_led, global_brightness=MAX_BRIGHTNESS, 78 | order='rgb', bus=0, device=1, max_speed_hz=8000000): 79 | self.num_led = num_led # The number of LEDs in the Strip 80 | order = order.lower() 81 | self.rgb = RGB_MAP.get(order, RGB_MAP['rgb']) 82 | # Limit the brightness to the maximum if it's set higher 83 | if global_brightness > self.MAX_BRIGHTNESS: 84 | self.global_brightness = self.MAX_BRIGHTNESS 85 | else: 86 | self.global_brightness = global_brightness 87 | 88 | self.leds = [self.LED_START,0,0,0] * self.num_led # Pixel buffer 89 | self.spi = spidev.SpiDev() # Init the SPI device 90 | self.spi.open(bus, device) # Open SPI port 0, slave device (CS) 1 91 | # Up the speed a bit, so that the LEDs are painted faster 92 | if max_speed_hz: 93 | self.spi.max_speed_hz = max_speed_hz 94 | 95 | def clock_start_frame(self): 96 | """Sends a start frame to the LED strip. 97 | 98 | This method clocks out a start frame, telling the receiving LED 99 | that it must update its own color now. 100 | """ 101 | self.spi.xfer2([0] * 4) # Start frame, 32 zero bits 102 | 103 | 104 | def clock_end_frame(self): 105 | """Sends an end frame to the LED strip. 106 | 107 | As explained above, dummy data must be sent after the last real colour 108 | information so that all of the data can reach its destination down the line. 109 | The delay is not as bad as with the human example above. 110 | It is only 1/2 bit per LED. This is because the SPI clock line 111 | needs to be inverted. 112 | 113 | Say a bit is ready on the SPI data line. The sender communicates 114 | this by toggling the clock line. The bit is read by the LED 115 | and immediately forwarded to the output data line. When the clock goes 116 | down again on the input side, the LED will toggle the clock up 117 | on the output to tell the next LED that the bit is ready. 118 | 119 | After one LED the clock is inverted, and after two LEDs it is in sync 120 | again, but one cycle behind. Therefore, for every two LEDs, one bit 121 | of delay gets accumulated. For 300 LEDs, 150 additional bits must be fed to 122 | the input of LED one so that the data can reach the last LED. 123 | 124 | Ultimately, we need to send additional numLEDs/2 arbitrary data bits, 125 | in order to trigger numLEDs/2 additional clock changes. This driver 126 | sends zeroes, which has the benefit of getting LED one partially or 127 | fully ready for the next update to the strip. An optimized version 128 | of the driver could omit the "clockStartFrame" method if enough zeroes have 129 | been sent as part of "clockEndFrame". 130 | """ 131 | # Round up num_led/2 bits (or num_led/16 bytes) 132 | for _ in range((self.num_led + 15) // 16): 133 | self.spi.xfer2([0x00]) 134 | 135 | 136 | def clear_strip(self): 137 | """ Turns off the strip and shows the result right away.""" 138 | 139 | for led in range(self.num_led): 140 | self.set_pixel(led, 0, 0, 0) 141 | self.show() 142 | 143 | 144 | def set_pixel(self, led_num, red, green, blue, bright_percent=100): 145 | """Sets the color of one pixel in the LED stripe. 146 | 147 | The changed pixel is not shown yet on the Stripe, it is only 148 | written to the pixel buffer. Colors are passed individually. 149 | If brightness is not set the global brightness setting is used. 150 | """ 151 | if led_num < 0: 152 | return # Pixel is invisible, so ignore 153 | if led_num >= self.num_led: 154 | return # again, invisible 155 | 156 | # Calculate pixel brightness as a percentage of the 157 | # defined global_brightness. Round up to nearest integer 158 | # as we expect some brightness unless set to 0 159 | brightness = ceil(bright_percent*self.global_brightness/100.0) 160 | brightness = int(brightness) 161 | 162 | # LED startframe is three "1" bits, followed by 5 brightness bits 163 | ledstart = (brightness & 0b00011111) | self.LED_START 164 | 165 | start_index = 4 * led_num 166 | self.leds[start_index] = ledstart 167 | self.leds[start_index + self.rgb[0]] = red 168 | self.leds[start_index + self.rgb[1]] = green 169 | self.leds[start_index + self.rgb[2]] = blue 170 | 171 | 172 | def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100): 173 | """Sets the color of one pixel in the LED stripe. 174 | 175 | The changed pixel is not shown yet on the Stripe, it is only 176 | written to the pixel buffer. 177 | Colors are passed combined (3 bytes concatenated) 178 | If brightness is not set the global brightness setting is used. 179 | """ 180 | self.set_pixel(led_num, (rgb_color & 0xFF0000) >> 16, 181 | (rgb_color & 0x00FF00) >> 8, rgb_color & 0x0000FF, 182 | bright_percent) 183 | 184 | 185 | def rotate(self, positions=1): 186 | """ Rotate the LEDs by the specified number of positions. 187 | 188 | Treating the internal LED array as a circular buffer, rotate it by 189 | the specified number of positions. The number could be negative, 190 | which means rotating in the opposite direction. 191 | """ 192 | cutoff = 4 * (positions % self.num_led) 193 | self.leds = self.leds[cutoff:] + self.leds[:cutoff] 194 | 195 | 196 | def show(self): 197 | """Sends the content of the pixel buffer to the strip. 198 | 199 | Todo: More than 1024 LEDs requires more than one xfer operation. 200 | """ 201 | self.clock_start_frame() 202 | # xfer2 kills the list, unfortunately. So it must be copied first 203 | # SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs. 204 | self.spi.xfer2(list(self.leds)) 205 | self.clock_end_frame() 206 | 207 | 208 | def cleanup(self): 209 | """Release the SPI device; Call this method at the end""" 210 | 211 | self.spi.close() # Close SPI port 212 | 213 | @staticmethod 214 | def combine_color(red, green, blue): 215 | """Make one 3*8 byte color value.""" 216 | 217 | return (red << 16) + (green << 8) + blue 218 | 219 | 220 | def wheel(self, wheel_pos): 221 | """Get a color from a color wheel; Green -> Red -> Blue -> Green""" 222 | 223 | if wheel_pos > 255: 224 | wheel_pos = 255 # Safeguard 225 | if wheel_pos < 85: # Green -> Red 226 | return self.combine_color(wheel_pos * 3, 255 - wheel_pos * 3, 0) 227 | if wheel_pos < 170: # Red -> Blue 228 | wheel_pos -= 85 229 | return self.combine_color(255 - wheel_pos * 3, 0, wheel_pos * 3) 230 | # Blue -> Green 231 | wheel_pos -= 170 232 | return self.combine_color(0, wheel_pos * 3, 255 - wheel_pos * 3) 233 | 234 | 235 | def dump_array(self): 236 | """For debug purposes: Dump the LED array onto the console.""" 237 | 238 | print(self.leds) 239 | --------------------------------------------------------------------------------