diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/act.py b/act.py old mode 100644 new mode 100755 index d7b2ef3..0437fa5 --- a/act.py +++ b/act.py @@ -19,15 +19,15 @@ from time import sleep import serial from pixel_ring import pixel_ring -ser = serial.Serial('/dev/ttyACM0', 1000000) # Establish the connection on a specific port +ser = serial.Serial('/dev/ttyACM0', 9600) # Establish the connection on a specific port def led_on(speaker): if speaker == 'mono3': - ser.write(b'3') + ser.write(b'H') if speaker == 'mono1': - ser.write(b'1') + ser.write(b'H') if speaker == 'mono2': pixel_ring.speak() @@ -35,10 +35,10 @@ def led_on(speaker): def led_off(speaker): if speaker == 'mono3': - ser.write(b'4') + ser.write(b'L') if speaker == 'mono1': - ser.write(b'2') + ser.write(b'L') if speaker == 'mono2': pixel_ring.off() diff --git a/config.py b/config.py old mode 100644 new mode 100755 index ba22a45..9fddc1b --- a/config.py +++ b/config.py @@ -8,7 +8,7 @@ # --- # Dictionary to link characters to the right voice -characters = {"ROGUE":["cmu-slt-hsmm", "mono2"], "SAINT":["dfki-obadiah-hsmm", "mono3"], "RASA":["dfki-poppy-hsmm", "mono1"] } +characters = {"ROGUE":["cmu-slt-hsmm", "mono2"], "SAINT":["cmu-rms-hsmm", "mono3"], "RASA":["dfki-poppy-hsmm", "mono1"] } # Dictionary to link stage directions to a particular formal action directions = {"Listen to Google Home":'listen_google_home','Music':'music'} diff --git a/logic.py b/logic.py old mode 100644 new mode 100755 index e8c840e..98c1ec8 --- a/logic.py +++ b/logic.py @@ -60,12 +60,15 @@ def tts(voice, input_text, speaker): if speaker =="mono1": volume_level = "amount:1.0" + #stadium_level = "amount:10.0" if speaker == "mono2": volume_level = "amount:0.7" + #stadium_level = "amount:10.0" if speaker == "mono3": - volume_level = "amount:0.8" + volume_level = "amount:0.9" + #stadium_level = "amount:85.0" @@ -75,6 +78,8 @@ def tts(voice, input_text, speaker): "LOCALE":"en_GB", "effect_VOLUME_selected":"on", "effect_VOLUME_parameters":volume_level, + #"effect_STADIUM_selected":"on", + #"effect_STADIUM_parameters":stadium_level, "VOICE": voice, # Voice informations (need to be compatible) "OUTPUT_TYPE":"AUDIO", "AUDIO":"WAVE", # Audio informations (need both) @@ -103,72 +108,35 @@ def tts(voice, input_text, speaker): raise Exception(content) # 04 Listen to Google Home -from tuning import Tuning -import usb.core -import usb.util -import time - -def listen(): - dev = usb.core.find(idVendor=0x2886, idProduct=0x0018) - - if dev: - Mic_tuning = Tuning(dev) - VAD = Mic_tuning.is_voice() - counter=0 - - time.sleep(2) - - voice_detected = 1 - - - while voice_detected == 1: - print('Google Home is Speaking') - time.sleep(4) - print(VAD) - - VAD = Mic_tuning.is_voice() - if VAD == 1: - counter = 0 - print('still speaking') - - if VAD == 0: - counter+=1 - print('silence detected') - - if counter == 2: - print('no voice detected') - voice_detected = 0 - - time.sleep(1) - - - print('Google Home is done') # 05 CONTROL THE LED OF THE SPEAKERS import serial from pixel_ring import pixel_ring -ser = serial.Serial('/dev/ttyACM0', 1000000) # Establish the connection on a specific port -def led_on(speaker): - if speaker == 'mono3': - ser.write(b'3') - - if speaker == 'mono1': - ser.write(b'1') - - if speaker == 'mono2': - pixel_ring.speak() +def led_on(ser, speaker): -def led_off(speaker): + if ser: + if speaker == 'mono3': + ser.write(b'A') + + if speaker == 'mono1': + ser.write(b'C') + + if speaker == 'mono2': + pixel_ring.speak() + +def led_off(ser, speaker): + + if ser: + if speaker == 'mono3': + ser.write(b'B') + + if speaker == 'mono1': + ser.write(b'D') + + if speaker == 'mono2': + pixel_ring.off() - if speaker == 'mono3': - ser.write(b'4') - - if speaker == 'mono1': - ser.write(b'2') - - if speaker == 'mono2': - pixel_ring.off() diff --git a/pixel_ring b/pixel_ring deleted file mode 160000 index 30de559..0000000 --- a/pixel_ring +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 30de55966fdf0d0f1ee6e02cf356c56ba76b577b diff --git a/pixel_ring/README.md b/pixel_ring/README.md new file mode 100644 index 0000000..51a524c --- /dev/null +++ b/pixel_ring/README.md @@ -0,0 +1,26 @@ +Pixel Ring +========== + +[![Build Status](https://travis-ci.org/respeaker/pixel_ring.svg?branch=master)](https://travis-ci.org/respeaker/pixel_ring) +[![Pypi](https://img.shields.io/pypi/v/pixel_ring.svg)](https://pypi.python.org/pypi/pixel_ring) + + +The library is for pixel ring based on APA102, ReSpeaker series pixel ring. + +## Hardware ++ ReSpeaker 4 Mic Array or ReSpeaker V2 ++ ReSpeaker V2 ++ ReSpeaker USB 6+1 Mic Array ++ ReSpeaker USB 4 Mic Array + +## Get started +``` +git clone --depth 1 https://github.com/respeaker/pixel_ring.git +cd pixel_ring +pip install -U -e . +python examples/respeaker_4mic_array.py +``` + + +## Credits ++ [APA102_Pi](https://github.com/tinue/APA102_Pi) diff --git a/pixel_ring/__init__.py b/pixel_ring/__init__.py new file mode 100755 index 0000000..fedbbb1 --- /dev/null +++ b/pixel_ring/__init__.py @@ -0,0 +1,52 @@ + + +from . import usb_pixel_ring_v1 +from . import usb_pixel_ring_v2 +from .apa102_pixel_ring import PixelRing + +pixel_ring = usb_pixel_ring_v2.find() + +if not pixel_ring: + pixel_ring = usb_pixel_ring_v1.find() + +if not pixel_ring: + pixel_ring = PixelRing() + + +USAGE = ''' +If the hardware is ReSpeaker 4 Mic Array for Pi or ReSpeaker V2, +there is a power-enable pin which should be enabled at first. ++ ReSpeaker 4 Mic Array for Pi: + + import gpiozero + power = LED(5) + power.on() + ++ ReSpeaker V2: + + import mraa + power = mraa.Gpio(12) + power.dir(mraa.DIR_OUT) + power.write(0) +''' + +def main(): + import time + + if isinstance(pixel_ring, usb_pixel_ring_v2.PixelRing): + print('Found ReSpeaker USB 4 Mic Array') + elif isinstance(pixel_ring, usb_pixel_ring_v1.UsbPixelRing): + print('Found ReSpeaker USB 6+1 Mic Array') + else: + print('Control APA102 RGB LEDs via SPI') + print(USAGE) + + pixel_ring.think() + time.sleep(3) + pixel_ring.off() + time.sleep(1) + + +if __name__ == '__main__': + main() + diff --git a/pixel_ring/apa102.py b/pixel_ring/apa102.py new file mode 100755 index 0000000..e5918b9 --- /dev/null +++ b/pixel_ring/apa102.py @@ -0,0 +1,243 @@ +""" +from https://github.com/tinue/APA102_Pi +This is the main driver module for APA102 LEDs +""" +import spidev +from math import ceil + +RGB_MAP = { 'rgb': [3, 2, 1], 'rbg': [3, 1, 2], 'grb': [2, 3, 1], + 'gbr': [2, 1, 3], 'brg': [1, 3, 2], 'bgr': [1, 2, 3] } + +class APA102: + """ + Driver for APA102 LEDS (aka "DotStar"). + + (c) Martin Erzberger 2016-2017 + + My very first Python code, so I am sure there is a lot to be optimized ;) + + Public methods are: + - set_pixel + - set_pixel_rgb + - show + - clear_strip + - cleanup + + Helper methods for color manipulation are: + - combine_color + - wheel + + The rest of the methods are used internally and should not be used by the + user of the library. + + Very brief overview of APA102: An APA102 LED is addressed with SPI. The bits + are shifted in one by one, starting with the least significant bit. + + An LED usually just forwards everything that is sent to its data-in to + data-out. While doing this, it remembers its own color and keeps glowing + with that color as long as there is power. + + An LED can be switched to not forward the data, but instead use the data + to change it's own color. This is done by sending (at least) 32 bits of + zeroes to data-in. The LED then accepts the next correct 32 bit LED + frame (with color information) as its new color setting. + + After having received the 32 bit color frame, the LED changes color, + and then resumes to just copying data-in to data-out. + + The really clever bit is this: While receiving the 32 bit LED frame, + the LED sends zeroes on its data-out line. Because a color frame is + 32 bits, the LED sends 32 bits of zeroes to the next LED. + As we have seen above, this means that the next LED is now ready + to accept a color frame and update its color. + + So that's really the entire protocol: + - Start by sending 32 bits of zeroes. This prepares LED 1 to update + its color. + - Send color information one by one, starting with the color for LED 1, + then LED 2 etc. + - Finish off by cycling the clock line a few times to get all data + to the very last LED on the strip + + The last step is necessary, because each LED delays forwarding the data + a bit. Imagine ten people in a row. When you yell the last color + information, i.e. the one for person ten, to the first person in + the line, then you are not finished yet. Person one has to turn around + and yell it to person 2, and so on. So it takes ten additional "dummy" + cycles until person ten knows the color. When you look closer, + you will see that not even person 9 knows its own color yet. This + information is still with person 2. Essentially the driver sends additional + zeroes to LED 1 as long as it takes for the last color frame to make it + down the line to the last LED. + """ + # Constants + MAX_BRIGHTNESS = 0b11111 # Safeguard: Set to a value appropriate for your setup + LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits + + def __init__(self, num_led, global_brightness=MAX_BRIGHTNESS, + order='rgb', bus=0, device=1, max_speed_hz=8000000): + self.num_led = num_led # The number of LEDs in the Strip + order = order.lower() + self.rgb = RGB_MAP.get(order, RGB_MAP['rgb']) + # Limit the brightness to the maximum if it's set higher + if global_brightness > self.MAX_BRIGHTNESS: + self.global_brightness = self.MAX_BRIGHTNESS + else: + self.global_brightness = global_brightness + + self.leds = [self.LED_START,0,0,0] * self.num_led # Pixel buffer + self.spi = spidev.SpiDev() # Init the SPI device + self.spi.open(bus, device) # Open SPI port 0, slave device (CS) 1 + # Up the speed a bit, so that the LEDs are painted faster + if max_speed_hz: + self.spi.max_speed_hz = max_speed_hz + + def clock_start_frame(self): + """Sends a start frame to the LED strip. + + This method clocks out a start frame, telling the receiving LED + that it must update its own color now. + """ + self.spi.xfer2([0] * 4) # Start frame, 32 zero bits + + + def clock_end_frame(self): + """Sends an end frame to the LED strip. + + As explained above, dummy data must be sent after the last real colour + information so that all of the data can reach its destination down the line. + The delay is not as bad as with the human example above. + It is only 1/2 bit per LED. This is because the SPI clock line + needs to be inverted. + + Say a bit is ready on the SPI data line. The sender communicates + this by toggling the clock line. The bit is read by the LED + and immediately forwarded to the output data line. When the clock goes + down again on the input side, the LED will toggle the clock up + on the output to tell the next LED that the bit is ready. + + After one LED the clock is inverted, and after two LEDs it is in sync + again, but one cycle behind. Therefore, for every two LEDs, one bit + of delay gets accumulated. For 300 LEDs, 150 additional bits must be fed to + the input of LED one so that the data can reach the last LED. + + Ultimately, we need to send additional numLEDs/2 arbitrary data bits, + in order to trigger numLEDs/2 additional clock changes. This driver + sends zeroes, which has the benefit of getting LED one partially or + fully ready for the next update to the strip. An optimized version + of the driver could omit the "clockStartFrame" method if enough zeroes have + been sent as part of "clockEndFrame". + """ + + self.spi.xfer2([0xFF] * 4) + + # Round up num_led/2 bits (or num_led/16 bytes) + #for _ in range((self.num_led + 15) // 16): + # self.spi.xfer2([0x00]) + + + def clear_strip(self): + """ Turns off the strip and shows the result right away.""" + + for led in range(self.num_led): + self.set_pixel(led, 0, 0, 0) + self.show() + + + def set_pixel(self, led_num, red, green, blue, bright_percent=100): + """Sets the color of one pixel in the LED stripe. + + The changed pixel is not shown yet on the Stripe, it is only + written to the pixel buffer. Colors are passed individually. + If brightness is not set the global brightness setting is used. + """ + if led_num < 0: + return # Pixel is invisible, so ignore + if led_num >= self.num_led: + return # again, invisible + + # Calculate pixel brightness as a percentage of the + # defined global_brightness. Round up to nearest integer + # as we expect some brightness unless set to 0 + brightness = int(ceil(bright_percent*self.global_brightness/100.0)) + + # LED startframe is three "1" bits, followed by 5 brightness bits + ledstart = (brightness & 0b00011111) | self.LED_START + + start_index = 4 * led_num + self.leds[start_index] = ledstart + self.leds[start_index + self.rgb[0]] = red + self.leds[start_index + self.rgb[1]] = green + self.leds[start_index + self.rgb[2]] = blue + + + def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100): + """Sets the color of one pixel in the LED stripe. + + The changed pixel is not shown yet on the Stripe, it is only + written to the pixel buffer. + Colors are passed combined (3 bytes concatenated) + If brightness is not set the global brightness setting is used. + """ + self.set_pixel(led_num, (rgb_color & 0xFF0000) >> 16, + (rgb_color & 0x00FF00) >> 8, rgb_color & 0x0000FF, + bright_percent) + + + def rotate(self, positions=1): + """ Rotate the LEDs by the specified number of positions. + + Treating the internal LED array as a circular buffer, rotate it by + the specified number of positions. The number could be negative, + which means rotating in the opposite direction. + """ + cutoff = 4 * (positions % self.num_led) + self.leds = self.leds[cutoff:] + self.leds[:cutoff] + + + def show(self): + """Sends the content of the pixel buffer to the strip. + + Todo: More than 1024 LEDs requires more than one xfer operation. + """ + self.clock_start_frame() + # xfer2 kills the list, unfortunately. So it must be copied first + # SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs. + data = list(self.leds) + while data: + self.spi.xfer2(data[:32]) + data = data[32:] + self.clock_end_frame() + + + def cleanup(self): + """Release the SPI device; Call this method at the end""" + + self.spi.close() # Close SPI port + + @staticmethod + def combine_color(red, green, blue): + """Make one 3*8 byte color value.""" + + return (red << 16) + (green << 8) + blue + + + def wheel(self, wheel_pos): + """Get a color from a color wheel; Green -> Red -> Blue -> Green""" + + if wheel_pos > 255: + wheel_pos = 255 # Safeguard + if wheel_pos < 85: # Green -> Red + return self.combine_color(wheel_pos * 3, 255 - wheel_pos * 3, 0) + if wheel_pos < 170: # Red -> Blue + wheel_pos -= 85 + return self.combine_color(255 - wheel_pos * 3, 0, wheel_pos * 3) + # Blue -> Green + wheel_pos -= 170 + return self.combine_color(0, wheel_pos * 3, 255 - wheel_pos * 3) + + + def dump_array(self): + """For debug purposes: Dump the LED array onto the console.""" + + print(self.leds) diff --git a/pixel_ring/apa102_pixel_ring.py b/pixel_ring/apa102_pixel_ring.py new file mode 100755 index 0000000..8690ba2 --- /dev/null +++ b/pixel_ring/apa102_pixel_ring.py @@ -0,0 +1,105 @@ + +import time +import threading +try: + import queue as Queue +except ImportError: + import Queue as Queue + +from .apa102 import APA102 +from .pattern import Echo, GoogleHome + + +class PixelRing(object): + PIXELS_N = 12 + + def __init__(self, pattern='google'): + if pattern == 'echo': + self.pattern = Echo(show=self.show) + else: + self.pattern = GoogleHome(show=self.show) + + self.dev = APA102(num_led=self.PIXELS_N) + + self.queue = Queue.Queue() + self.thread = threading.Thread(target=self._run) + self.thread.daemon = True + self.thread.start() + self.off() + + def set_brightness(self, brightness): + if brightness > 100: + brightness = 100 + + if brightness > 0: + self.dev.global_brightness = int(0b11111 * brightness / 100) + + def change_pattern(self, pattern): + if pattern == 'echo': + self.pattern = Echo(show=self.show) + else: + self.pattern = GoogleHome(show=self.show) + + def wakeup(self, direction=0): + def f(): + self.pattern.wakeup(direction) + + self.put(f) + + def listen(self): + self.put(self.pattern.listen) + + def think(self): + self.put(self.pattern.think) + + wait = think + + def speak(self): + self.put(self.pattern.speak) + + def off(self): + self.put(self.pattern.off) + + def put(self, func): + self.pattern.stop = True + self.queue.put(func) + + def _run(self): + while True: + func = self.queue.get() + self.pattern.stop = False + func() + + def show(self, data): + for i in range(self.PIXELS_N): + self.dev.set_pixel(i, int(data[4*i + 1]), int(data[4*i + 2]), int(data[4*i + 3])) + + self.dev.show() + + def set_color(self, rgb=None, r=0, g=0, b=0): + if rgb: + r, g, b = (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF + for i in range(self.PIXELS_N): + self.dev.set_pixel(i, r, g, b) + + self.dev.show() + + +if __name__ == '__main__': + pixel_ring = PixelRing() + while True: + try: + pixel_ring.wakeup() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.speak() + time.sleep(6) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + + pixel_ring.off() + time.sleep(1) diff --git a/pixel_ring/examples/echo_pattern.py b/pixel_ring/examples/echo_pattern.py new file mode 100644 index 0000000..763c800 --- /dev/null +++ b/pixel_ring/examples/echo_pattern.py @@ -0,0 +1,37 @@ +""" +Control pixel ring on ReSpeaker 4 Mic Array + +pip install pixel_ring gpiozero +""" + +import time + +from pixel_ring import pixel_ring +from gpiozero import LED + + +if __name__ == '__main__': + power = LED(5) + power.on() + + pixel_ring.set_brightness(20) + pixel_ring.change_pattern('echo') + while True: + + try: + pixel_ring.wakeup() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.speak() + time.sleep(6) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + + pixel_ring.off() + power.off() + time.sleep(1) + diff --git a/pixel_ring/examples/respeaker_4mic_array.py b/pixel_ring/examples/respeaker_4mic_array.py new file mode 100644 index 0000000..d339095 --- /dev/null +++ b/pixel_ring/examples/respeaker_4mic_array.py @@ -0,0 +1,36 @@ +""" +Control pixel ring on ReSpeaker 4 Mic Array + +pip install pixel_ring gpiozero +""" + +import time + +from pixel_ring import pixel_ring +from gpiozero import LED + +power = LED(5) +power.on() + +pixel_ring.set_brightness(10) + +if __name__ == '__main__': + while True: + + try: + pixel_ring.wakeup() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.speak() + time.sleep(6) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + + pixel_ring.off() + time.sleep(1) + +power.off() diff --git a/pixel_ring/examples/respeaker_v2_pixel_ring.py b/pixel_ring/examples/respeaker_v2_pixel_ring.py new file mode 100644 index 0000000..4e07b5b --- /dev/null +++ b/pixel_ring/examples/respeaker_v2_pixel_ring.py @@ -0,0 +1,43 @@ +""" +Control pixel ring on ReSpeaker V2 + +sudo apt install python-mraa libmraa1 +pip install pixel-ring + +""" + +import time + +from pixel_ring import pixel_ring +import mraa +import os + +en = mraa.Gpio(12) +if os.geteuid() != 0 : + time.sleep(1) + +en.dir(mraa.DIR_OUT) +en.write(0) + +pixel_ring.set_brightness(20) + +if __name__ == '__main__': + while True: + + try: + pixel_ring.wakeup() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.speak() + time.sleep(6) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + + pixel_ring.off() + time.sleep(1) + +en.write(1) diff --git a/pixel_ring/examples/usb_mic_array.py b/pixel_ring/examples/usb_mic_array.py new file mode 100644 index 0000000..f09b580 --- /dev/null +++ b/pixel_ring/examples/usb_mic_array.py @@ -0,0 +1,29 @@ +""" +Control pixel ring on ReSpeaker USB Mic Array +""" + +import time + +from pixel_ring import pixel_ring + + +if __name__ == '__main__': + pixel_ring.change_pattern('echo') + while True: + + try: + pixel_ring.wakeup() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.speak() + time.sleep(6) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + + pixel_ring.off() + time.sleep(1) + diff --git a/pixel_ring/pattern.py b/pixel_ring/pattern.py new file mode 100755 index 0000000..054910f --- /dev/null +++ b/pixel_ring/pattern.py @@ -0,0 +1,145 @@ +""" +LED pattern like Echo +""" + +import time + + +class Echo(object): + brightness = 24 * 8 + + def __init__(self, show, number=12): + self.pixels_number = number + self.pixels = [0] * 4 * number + + if not callable(show): + raise ValueError('show parameter is not callable') + + self.show = show + self.stop = False + + def wakeup(self, direction=0): + position = int((direction + 15) / (360 / self.pixels_number)) % self.pixels_number + + pixels = [0, 0, 0, self.brightness] * self.pixels_number + pixels[position * 4 + 2] = self.brightness + + self.show(pixels) + + def listen(self): + pixels = [0, 0, 0, self.brightness] * self.pixels_number + + self.show(pixels) + + def think(self): + half_brightness = int(self.brightness / 2) + pixels = [0, 0, half_brightness, half_brightness, 0, 0, 0, self.brightness] * self.pixels_number + + while not self.stop: + self.show(pixels) + time.sleep(0.2) + pixels = pixels[-4:] + pixels[:-4] + + def speak(self): + step = int(self.brightness / 12) + position = int(self.brightness / 2) + while not self.stop: + pixels = [0, 0, position, self.brightness - position] * self.pixels_number + self.show(pixels) + time.sleep(0.01) + if position <= 0: + step = int(self.brightness / 12) + time.sleep(0.4) + elif position >= int(self.brightness / 2): + step = - int(self.brightness / 12) + time.sleep(0.4) + + position += step + + def off(self): + self.show([0] * 4 * 12) + +class GoogleHome(object): + def __init__(self, show): + self.basis = [0] * 4 * 12 + self.basis[0 * 4 + 1] = 8 + self.basis[3 * 4 + 1] = 4 + self.basis[3 * 4 + 2] = 4 + self.basis[6 * 4 + 2] = 8 + self.basis[9 * 4 + 3] = 8 + + self.pixels = self.basis + + if not callable(show): + raise ValueError('show parameter is not callable') + + self.show = show + self.stop = False + + def wakeup(self, direction=0): + position = int((direction + 90 + 15) / 30) % 12 + + basis = self.basis[position*-4:] + self.basis[:position*-4] + + pixels = [v * 25 for v in basis] + self.show(pixels) + time.sleep(0.1) + + pixels = pixels[-4:] + pixels[:-4] + self.show(pixels) + time.sleep(0.1) + + for i in range(2): + new_pixels = pixels[-4:] + pixels[:-4] + + self.show([v/2+pixels[index] for index, v in enumerate(new_pixels)]) + pixels = new_pixels + time.sleep(0.1) + + self.show(pixels) + self.pixels = pixels + + def listen(self): + pixels = self.pixels + for i in range(1, 25): + self.show([(v * i / 24) for v in pixels]) + time.sleep(0.01) + + def think(self): + pixels = self.pixels + + while not self.stop: + pixels = pixels[-4:] + pixels[:-4] + self.show(pixels) + time.sleep(0.2) + + t = 0.1 + for i in range(0, 5): + pixels = pixels[-4:] + pixels[:-4] + self.show([(v * (4 - i) / 4) for v in pixels]) + time.sleep(t) + t /= 2 + + self.pixels = pixels + + def speak(self): + pixels = self.pixels + step = 1 + brightness = 5 + while not self.stop: + self.show([(v * brightness / 24) for v in pixels]) + time.sleep(0.02) + + if brightness <= 5: + step = 1 + time.sleep(0.4) + elif brightness >= 24: + step = -1 + time.sleep(0.4) + + brightness += step + + def off(self): + self.show([0] * 4 * 12) + + diff --git a/pixel_ring/pixel_ring.py b/pixel_ring/pixel_ring.py new file mode 100755 index 0000000..0f68f68 --- /dev/null +++ b/pixel_ring/pixel_ring.py @@ -0,0 +1,26 @@ + + +class PixelRing(object): + def __init__(self): + pass + + def show(self, data): + pass + + def set_color(self, rgb=None, r=0, g=0, b=0): + pass + + def wakeup(self, angle=None): + pass + + def listen(self): + pass + + def think(self): + pass + + def speak(self): + pass + + def off(self): + pass diff --git a/pixel_ring/pixel_ring/__init__.py b/pixel_ring/pixel_ring/__init__.py new file mode 100644 index 0000000..fedbbb1 --- /dev/null +++ b/pixel_ring/pixel_ring/__init__.py @@ -0,0 +1,52 @@ + + +from . import usb_pixel_ring_v1 +from . import usb_pixel_ring_v2 +from .apa102_pixel_ring import PixelRing + +pixel_ring = usb_pixel_ring_v2.find() + +if not pixel_ring: + pixel_ring = usb_pixel_ring_v1.find() + +if not pixel_ring: + pixel_ring = PixelRing() + + +USAGE = ''' +If the hardware is ReSpeaker 4 Mic Array for Pi or ReSpeaker V2, +there is a power-enable pin which should be enabled at first. ++ ReSpeaker 4 Mic Array for Pi: + + import gpiozero + power = LED(5) + power.on() + ++ ReSpeaker V2: + + import mraa + power = mraa.Gpio(12) + power.dir(mraa.DIR_OUT) + power.write(0) +''' + +def main(): + import time + + if isinstance(pixel_ring, usb_pixel_ring_v2.PixelRing): + print('Found ReSpeaker USB 4 Mic Array') + elif isinstance(pixel_ring, usb_pixel_ring_v1.UsbPixelRing): + print('Found ReSpeaker USB 6+1 Mic Array') + else: + print('Control APA102 RGB LEDs via SPI') + print(USAGE) + + pixel_ring.think() + time.sleep(3) + pixel_ring.off() + time.sleep(1) + + +if __name__ == '__main__': + main() + diff --git a/pixel_ring/pixel_ring/apa102.py b/pixel_ring/pixel_ring/apa102.py new file mode 100755 index 0000000..e5918b9 --- /dev/null +++ b/pixel_ring/pixel_ring/apa102.py @@ -0,0 +1,243 @@ +""" +from https://github.com/tinue/APA102_Pi +This is the main driver module for APA102 LEDs +""" +import spidev +from math import ceil + +RGB_MAP = { 'rgb': [3, 2, 1], 'rbg': [3, 1, 2], 'grb': [2, 3, 1], + 'gbr': [2, 1, 3], 'brg': [1, 3, 2], 'bgr': [1, 2, 3] } + +class APA102: + """ + Driver for APA102 LEDS (aka "DotStar"). + + (c) Martin Erzberger 2016-2017 + + My very first Python code, so I am sure there is a lot to be optimized ;) + + Public methods are: + - set_pixel + - set_pixel_rgb + - show + - clear_strip + - cleanup + + Helper methods for color manipulation are: + - combine_color + - wheel + + The rest of the methods are used internally and should not be used by the + user of the library. + + Very brief overview of APA102: An APA102 LED is addressed with SPI. The bits + are shifted in one by one, starting with the least significant bit. + + An LED usually just forwards everything that is sent to its data-in to + data-out. While doing this, it remembers its own color and keeps glowing + with that color as long as there is power. + + An LED can be switched to not forward the data, but instead use the data + to change it's own color. This is done by sending (at least) 32 bits of + zeroes to data-in. The LED then accepts the next correct 32 bit LED + frame (with color information) as its new color setting. + + After having received the 32 bit color frame, the LED changes color, + and then resumes to just copying data-in to data-out. + + The really clever bit is this: While receiving the 32 bit LED frame, + the LED sends zeroes on its data-out line. Because a color frame is + 32 bits, the LED sends 32 bits of zeroes to the next LED. + As we have seen above, this means that the next LED is now ready + to accept a color frame and update its color. + + So that's really the entire protocol: + - Start by sending 32 bits of zeroes. This prepares LED 1 to update + its color. + - Send color information one by one, starting with the color for LED 1, + then LED 2 etc. + - Finish off by cycling the clock line a few times to get all data + to the very last LED on the strip + + The last step is necessary, because each LED delays forwarding the data + a bit. Imagine ten people in a row. When you yell the last color + information, i.e. the one for person ten, to the first person in + the line, then you are not finished yet. Person one has to turn around + and yell it to person 2, and so on. So it takes ten additional "dummy" + cycles until person ten knows the color. When you look closer, + you will see that not even person 9 knows its own color yet. This + information is still with person 2. Essentially the driver sends additional + zeroes to LED 1 as long as it takes for the last color frame to make it + down the line to the last LED. + """ + # Constants + MAX_BRIGHTNESS = 0b11111 # Safeguard: Set to a value appropriate for your setup + LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits + + def __init__(self, num_led, global_brightness=MAX_BRIGHTNESS, + order='rgb', bus=0, device=1, max_speed_hz=8000000): + self.num_led = num_led # The number of LEDs in the Strip + order = order.lower() + self.rgb = RGB_MAP.get(order, RGB_MAP['rgb']) + # Limit the brightness to the maximum if it's set higher + if global_brightness > self.MAX_BRIGHTNESS: + self.global_brightness = self.MAX_BRIGHTNESS + else: + self.global_brightness = global_brightness + + self.leds = [self.LED_START,0,0,0] * self.num_led # Pixel buffer + self.spi = spidev.SpiDev() # Init the SPI device + self.spi.open(bus, device) # Open SPI port 0, slave device (CS) 1 + # Up the speed a bit, so that the LEDs are painted faster + if max_speed_hz: + self.spi.max_speed_hz = max_speed_hz + + def clock_start_frame(self): + """Sends a start frame to the LED strip. + + This method clocks out a start frame, telling the receiving LED + that it must update its own color now. + """ + self.spi.xfer2([0] * 4) # Start frame, 32 zero bits + + + def clock_end_frame(self): + """Sends an end frame to the LED strip. + + As explained above, dummy data must be sent after the last real colour + information so that all of the data can reach its destination down the line. + The delay is not as bad as with the human example above. + It is only 1/2 bit per LED. This is because the SPI clock line + needs to be inverted. + + Say a bit is ready on the SPI data line. The sender communicates + this by toggling the clock line. The bit is read by the LED + and immediately forwarded to the output data line. When the clock goes + down again on the input side, the LED will toggle the clock up + on the output to tell the next LED that the bit is ready. + + After one LED the clock is inverted, and after two LEDs it is in sync + again, but one cycle behind. Therefore, for every two LEDs, one bit + of delay gets accumulated. For 300 LEDs, 150 additional bits must be fed to + the input of LED one so that the data can reach the last LED. + + Ultimately, we need to send additional numLEDs/2 arbitrary data bits, + in order to trigger numLEDs/2 additional clock changes. This driver + sends zeroes, which has the benefit of getting LED one partially or + fully ready for the next update to the strip. An optimized version + of the driver could omit the "clockStartFrame" method if enough zeroes have + been sent as part of "clockEndFrame". + """ + + self.spi.xfer2([0xFF] * 4) + + # Round up num_led/2 bits (or num_led/16 bytes) + #for _ in range((self.num_led + 15) // 16): + # self.spi.xfer2([0x00]) + + + def clear_strip(self): + """ Turns off the strip and shows the result right away.""" + + for led in range(self.num_led): + self.set_pixel(led, 0, 0, 0) + self.show() + + + def set_pixel(self, led_num, red, green, blue, bright_percent=100): + """Sets the color of one pixel in the LED stripe. + + The changed pixel is not shown yet on the Stripe, it is only + written to the pixel buffer. Colors are passed individually. + If brightness is not set the global brightness setting is used. + """ + if led_num < 0: + return # Pixel is invisible, so ignore + if led_num >= self.num_led: + return # again, invisible + + # Calculate pixel brightness as a percentage of the + # defined global_brightness. Round up to nearest integer + # as we expect some brightness unless set to 0 + brightness = int(ceil(bright_percent*self.global_brightness/100.0)) + + # LED startframe is three "1" bits, followed by 5 brightness bits + ledstart = (brightness & 0b00011111) | self.LED_START + + start_index = 4 * led_num + self.leds[start_index] = ledstart + self.leds[start_index + self.rgb[0]] = red + self.leds[start_index + self.rgb[1]] = green + self.leds[start_index + self.rgb[2]] = blue + + + def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100): + """Sets the color of one pixel in the LED stripe. + + The changed pixel is not shown yet on the Stripe, it is only + written to the pixel buffer. + Colors are passed combined (3 bytes concatenated) + If brightness is not set the global brightness setting is used. + """ + self.set_pixel(led_num, (rgb_color & 0xFF0000) >> 16, + (rgb_color & 0x00FF00) >> 8, rgb_color & 0x0000FF, + bright_percent) + + + def rotate(self, positions=1): + """ Rotate the LEDs by the specified number of positions. + + Treating the internal LED array as a circular buffer, rotate it by + the specified number of positions. The number could be negative, + which means rotating in the opposite direction. + """ + cutoff = 4 * (positions % self.num_led) + self.leds = self.leds[cutoff:] + self.leds[:cutoff] + + + def show(self): + """Sends the content of the pixel buffer to the strip. + + Todo: More than 1024 LEDs requires more than one xfer operation. + """ + self.clock_start_frame() + # xfer2 kills the list, unfortunately. So it must be copied first + # SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs. + data = list(self.leds) + while data: + self.spi.xfer2(data[:32]) + data = data[32:] + self.clock_end_frame() + + + def cleanup(self): + """Release the SPI device; Call this method at the end""" + + self.spi.close() # Close SPI port + + @staticmethod + def combine_color(red, green, blue): + """Make one 3*8 byte color value.""" + + return (red << 16) + (green << 8) + blue + + + def wheel(self, wheel_pos): + """Get a color from a color wheel; Green -> Red -> Blue -> Green""" + + if wheel_pos > 255: + wheel_pos = 255 # Safeguard + if wheel_pos < 85: # Green -> Red + return self.combine_color(wheel_pos * 3, 255 - wheel_pos * 3, 0) + if wheel_pos < 170: # Red -> Blue + wheel_pos -= 85 + return self.combine_color(255 - wheel_pos * 3, 0, wheel_pos * 3) + # Blue -> Green + wheel_pos -= 170 + return self.combine_color(0, wheel_pos * 3, 255 - wheel_pos * 3) + + + def dump_array(self): + """For debug purposes: Dump the LED array onto the console.""" + + print(self.leds) diff --git a/pixel_ring/pixel_ring/apa102_pixel_ring.py b/pixel_ring/pixel_ring/apa102_pixel_ring.py new file mode 100644 index 0000000..8690ba2 --- /dev/null +++ b/pixel_ring/pixel_ring/apa102_pixel_ring.py @@ -0,0 +1,105 @@ + +import time +import threading +try: + import queue as Queue +except ImportError: + import Queue as Queue + +from .apa102 import APA102 +from .pattern import Echo, GoogleHome + + +class PixelRing(object): + PIXELS_N = 12 + + def __init__(self, pattern='google'): + if pattern == 'echo': + self.pattern = Echo(show=self.show) + else: + self.pattern = GoogleHome(show=self.show) + + self.dev = APA102(num_led=self.PIXELS_N) + + self.queue = Queue.Queue() + self.thread = threading.Thread(target=self._run) + self.thread.daemon = True + self.thread.start() + self.off() + + def set_brightness(self, brightness): + if brightness > 100: + brightness = 100 + + if brightness > 0: + self.dev.global_brightness = int(0b11111 * brightness / 100) + + def change_pattern(self, pattern): + if pattern == 'echo': + self.pattern = Echo(show=self.show) + else: + self.pattern = GoogleHome(show=self.show) + + def wakeup(self, direction=0): + def f(): + self.pattern.wakeup(direction) + + self.put(f) + + def listen(self): + self.put(self.pattern.listen) + + def think(self): + self.put(self.pattern.think) + + wait = think + + def speak(self): + self.put(self.pattern.speak) + + def off(self): + self.put(self.pattern.off) + + def put(self, func): + self.pattern.stop = True + self.queue.put(func) + + def _run(self): + while True: + func = self.queue.get() + self.pattern.stop = False + func() + + def show(self, data): + for i in range(self.PIXELS_N): + self.dev.set_pixel(i, int(data[4*i + 1]), int(data[4*i + 2]), int(data[4*i + 3])) + + self.dev.show() + + def set_color(self, rgb=None, r=0, g=0, b=0): + if rgb: + r, g, b = (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF + for i in range(self.PIXELS_N): + self.dev.set_pixel(i, r, g, b) + + self.dev.show() + + +if __name__ == '__main__': + pixel_ring = PixelRing() + while True: + try: + pixel_ring.wakeup() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.speak() + time.sleep(6) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + + pixel_ring.off() + time.sleep(1) diff --git a/pixel_ring/pixel_ring/pattern.py b/pixel_ring/pixel_ring/pattern.py new file mode 100755 index 0000000..054910f --- /dev/null +++ b/pixel_ring/pixel_ring/pattern.py @@ -0,0 +1,145 @@ +""" +LED pattern like Echo +""" + +import time + + +class Echo(object): + brightness = 24 * 8 + + def __init__(self, show, number=12): + self.pixels_number = number + self.pixels = [0] * 4 * number + + if not callable(show): + raise ValueError('show parameter is not callable') + + self.show = show + self.stop = False + + def wakeup(self, direction=0): + position = int((direction + 15) / (360 / self.pixels_number)) % self.pixels_number + + pixels = [0, 0, 0, self.brightness] * self.pixels_number + pixels[position * 4 + 2] = self.brightness + + self.show(pixels) + + def listen(self): + pixels = [0, 0, 0, self.brightness] * self.pixels_number + + self.show(pixels) + + def think(self): + half_brightness = int(self.brightness / 2) + pixels = [0, 0, half_brightness, half_brightness, 0, 0, 0, self.brightness] * self.pixels_number + + while not self.stop: + self.show(pixels) + time.sleep(0.2) + pixels = pixels[-4:] + pixels[:-4] + + def speak(self): + step = int(self.brightness / 12) + position = int(self.brightness / 2) + while not self.stop: + pixels = [0, 0, position, self.brightness - position] * self.pixels_number + self.show(pixels) + time.sleep(0.01) + if position <= 0: + step = int(self.brightness / 12) + time.sleep(0.4) + elif position >= int(self.brightness / 2): + step = - int(self.brightness / 12) + time.sleep(0.4) + + position += step + + def off(self): + self.show([0] * 4 * 12) + +class GoogleHome(object): + def __init__(self, show): + self.basis = [0] * 4 * 12 + self.basis[0 * 4 + 1] = 8 + self.basis[3 * 4 + 1] = 4 + self.basis[3 * 4 + 2] = 4 + self.basis[6 * 4 + 2] = 8 + self.basis[9 * 4 + 3] = 8 + + self.pixels = self.basis + + if not callable(show): + raise ValueError('show parameter is not callable') + + self.show = show + self.stop = False + + def wakeup(self, direction=0): + position = int((direction + 90 + 15) / 30) % 12 + + basis = self.basis[position*-4:] + self.basis[:position*-4] + + pixels = [v * 25 for v in basis] + self.show(pixels) + time.sleep(0.1) + + pixels = pixels[-4:] + pixels[:-4] + self.show(pixels) + time.sleep(0.1) + + for i in range(2): + new_pixels = pixels[-4:] + pixels[:-4] + + self.show([v/2+pixels[index] for index, v in enumerate(new_pixels)]) + pixels = new_pixels + time.sleep(0.1) + + self.show(pixels) + self.pixels = pixels + + def listen(self): + pixels = self.pixels + for i in range(1, 25): + self.show([(v * i / 24) for v in pixels]) + time.sleep(0.01) + + def think(self): + pixels = self.pixels + + while not self.stop: + pixels = pixels[-4:] + pixels[:-4] + self.show(pixels) + time.sleep(0.2) + + t = 0.1 + for i in range(0, 5): + pixels = pixels[-4:] + pixels[:-4] + self.show([(v * (4 - i) / 4) for v in pixels]) + time.sleep(t) + t /= 2 + + self.pixels = pixels + + def speak(self): + pixels = self.pixels + step = 1 + brightness = 5 + while not self.stop: + self.show([(v * brightness / 24) for v in pixels]) + time.sleep(0.02) + + if brightness <= 5: + step = 1 + time.sleep(0.4) + elif brightness >= 24: + step = -1 + time.sleep(0.4) + + brightness += step + + def off(self): + self.show([0] * 4 * 12) + + diff --git a/pixel_ring/pixel_ring/pixel_ring.py b/pixel_ring/pixel_ring/pixel_ring.py new file mode 100644 index 0000000..0f68f68 --- /dev/null +++ b/pixel_ring/pixel_ring/pixel_ring.py @@ -0,0 +1,26 @@ + + +class PixelRing(object): + def __init__(self): + pass + + def show(self, data): + pass + + def set_color(self, rgb=None, r=0, g=0, b=0): + pass + + def wakeup(self, angle=None): + pass + + def listen(self): + pass + + def think(self): + pass + + def speak(self): + pass + + def off(self): + pass diff --git a/pixel_ring/pixel_ring/usb_pixel_ring_v1.py b/pixel_ring/pixel_ring/usb_pixel_ring_v1.py new file mode 100755 index 0000000..343d758 --- /dev/null +++ b/pixel_ring/pixel_ring/usb_pixel_ring_v1.py @@ -0,0 +1,190 @@ + +import usb.core +import usb.util + + +class HidDevice: + """ + This class provides basic functions to access + a USB HID device to write an endpoint + """ + + def __init__(self, dev, ep_in, ep_out): + self.dev = dev + self.ep_in = ep_in + self.ep_out = ep_out + + def write(self, data): + """ + write data on the OUT endpoint associated to the HID interface + """ + self.ep_out.write(data) + + def read(self): + return self.ep_in.read(self.ep_in.wMaxPacketSize, -1) + + def close(self): + """ + close the interface + """ + usb.util.dispose_resources(self.dev) + + @staticmethod + def find(vid=0x2886, pid=0x0007): + dev = usb.core.find(idVendor=vid, idProduct=pid) + if not dev: + return + + config = dev.get_active_configuration() + + # Iterate on all interfaces to find a HID interface + ep_in, ep_out = None, None + for interface in config: + if interface.bInterfaceClass == 0x03: + try: + if dev.is_kernel_driver_active(interface.bInterfaceNumber): + dev.detach_kernel_driver(interface.bInterfaceNumber) + except Exception as e: + print(e.message) + + for ep in interface: + if ep.bEndpointAddress & 0x80: + ep_in = ep + else: + ep_out = ep + break + + + + if ep_in and ep_out: + hid = HidDevice(dev, ep_in, ep_out) + + return hid + + +class UsbPixelRing: + PIXELS_N = 12 + + MONO = 1 + THINK = 3 + VOLUME = 5 + CUSTOM = 6 + + def __init__(self, hid=None, pattern=None): + self.hid = hid if hid else HidDevice.find() + if not self.hid: + print('No USB device found') + + colors = [0] * 4 * self.PIXELS_N + colors[0] = 0x4 + colors[1] = 0x40 + colors[2] = 0x4 + + colors[4 + 1] = 0x8 + colors[4 * 11 + 1] = 0x8 + + self.direction_template = colors + + def set_brightness(self, brightness): + print('Not support to change brightness') + + def change_pattern(self, pattern=None): + print('Not support to change pattern') + + def off(self): + self.set_color(rgb=0) + + def set_color(self, rgb=None, r=0, g=0, b=0): + if rgb: + self.write(0, [self.MONO, rgb & 0xFF, (rgb >> 8) & 0xFF, (rgb >> 16) & 0xFF]) + else: + self.write(0, [self.MONO, b, g, r]) + + def think(self): + self.write(0, [self.THINK, 0, 0, 0]) + + wait = think + + speak = think + + def set_volume(self, pixels): + self.write(0, [self.VOLUME, 0, 0, pixels]) + + def wakeup(self, angle=0): + if angle < 0 or angle > 360: + return + + position = int((angle + 15) % 360 / 30) % self.PIXELS_N + colors = self.direction_template[-position*4:] + self.direction_template[:-position*4] + + self.write(0, [self.CUSTOM, 0, 0, 0]) + self.write(3, colors) + + return position + + def listen(self, angle=0): + self.write(0, [self.MONO, 0, 0x10, 0]) + + def show(self, data): + self.write(0, [self.CUSTOM, 0, 0, 0]) + self.write(3, data) + + @staticmethod + def to_bytearray(data): + if type(data) is int: + array = bytearray([data & 0xFF]) + elif type(data) is bytearray: + array = data + elif type(data) is str or type(data) is bytes: + array = bytearray(data) + elif type(data) is list: + array = bytearray(data) + else: + raise TypeError('%s is not supported' % type(data)) + + return array + + def write(self, address, data): + data = self.to_bytearray(data) + length = len(data) + if self.hid: + packet = bytearray([address & 0xFF, (address >> 8) & 0xFF, length & 0xFF, (length >> 8) & 0xFF]) + data + self.hid.write(packet) + + def close(self): + if self.hid: + self.hid.close() + + def __call__(self, data): + self.write(3, data) + + +def find(): + hid = HidDevice.find() + + if hid: + pixel_ring = UsbPixelRing(hid) + return pixel_ring + + +if __name__ == '__main__': + import time + + pixel_ring = UsbPixelRing() + while True: + try: + pixel_ring.wakeup(180) + time.sleep(3) + pixel_ring.listen() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.set_volume(8) + time.sleep(3) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + pixel_ring.off() + diff --git a/pixel_ring/pixel_ring/usb_pixel_ring_v2.py b/pixel_ring/pixel_ring/usb_pixel_ring_v2.py new file mode 100755 index 0000000..b7a07a2 --- /dev/null +++ b/pixel_ring/pixel_ring/usb_pixel_ring_v2.py @@ -0,0 +1,122 @@ + +import usb.core +import usb.util + + +class PixelRing: + TIMEOUT = 8000 + + def __init__(self, dev): + self.dev = dev + + def trace(self): + self.write(0) + + def mono(self, color): + self.write(1, [(color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, 0]) + + def set_color(self, rgb=None, r=0, g=0, b=0): + if rgb: + self.mono(rgb) + else: + self.write(1, [r, g, b, 0]) + + def off(self): + self.mono(0) + + def listen(self, direction=None): + self.write(2) + + wakeup = listen + + def speak(self): + self.write(3) + + def think(self): + self.write(4) + + wait = think + + def spin(self): + self.write(5) + + def show(self, data): + self.write(6, data) + + customize = show + + def set_brightness(self, brightness): + self.write(0x20, [brightness]) + + def set_color_palette(self, a, b): + self.write(0x21, [(a >> 16) & 0xFF, (a >> 8) & 0xFF, a & 0xFF, 0, (b >> 16) & 0xFF, (b >> 8) & 0xFF, b & 0xFF, 0]) + + def set_vad_led(self, state): + self.write(0x22, [state]) + + def set_volume(self, volume): + self.write(0x23, [volume]) + + def change_pattern(self, pattern): + if pattern == 'echo': + self.write(0x24, [1]) + else: + self.write(0x24, [0]) + + def write(self, cmd, data=[0]): + self.dev.ctrl_transfer( + usb.util.CTRL_OUT | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE, + 0, cmd, 0x1C, data, self.TIMEOUT) + + @property + def version(self): + return self.dev.ctrl_transfer( + usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE, + 0, 0x80 | 0x40, 0x1C, 24, self.TIMEOUT).tostring() + + def close(self): + """ + close the interface + """ + usb.util.dispose_resources(self.dev) + + +def find(vid=0x2886, pid=0x0018): + dev = usb.core.find(idVendor=vid, idProduct=pid) + if not dev: + return + + # configuration = dev.get_active_configuration() + + # interface_number = None + # for interface in configuration: + # interface_number = interface.bInterfaceNumber + + # if dev.is_kernel_driver_active(interface_number): + # dev.detach_kernel_driver(interface_number) + + return PixelRing(dev) + + + +if __name__ == '__main__': + import time + + pixel_ring = find() + print(pixel_ring.version) + while True: + try: + pixel_ring.wakeup(180) + time.sleep(3) + pixel_ring.listen() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.set_volume(8) + time.sleep(3) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + pixel_ring.off() \ No newline at end of file diff --git a/pixel_ring/requirements.txt b/pixel_ring/requirements.txt new file mode 100644 index 0000000..a77bdae --- /dev/null +++ b/pixel_ring/requirements.txt @@ -0,0 +1,2 @@ +spidev +pyusb \ No newline at end of file diff --git a/pixel_ring/setup.cfg b/pixel_ring/setup.cfg new file mode 100644 index 0000000..2bfc54f --- /dev/null +++ b/pixel_ring/setup.cfg @@ -0,0 +1,3 @@ + +[bdist_wheel] +universal = 1 diff --git a/pixel_ring/setup.py b/pixel_ring/setup.py new file mode 100755 index 0000000..b84d034 --- /dev/null +++ b/pixel_ring/setup.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The setup script.""" + +from setuptools import setup, find_packages + +README = \ +''' +RGB LED library for ReSpeaker USB 6+1 Microphone Array, 4 Mic Array for Raspberry Pi +to control the pixel ring +''' + + +requirements = [ + 'spidev', + 'pyusb' +] + +setup_requirements = [ + # TODO: put setup requirements (distutils extensions, etc.) here +] + +test_requirements = [ + 'pytest' +] + +setup( + name='pixel-ring', + version='0.1.0', + description="respeaker series pixel ring library", + long_description=README, + author="Yihui Xiong", + author_email='yihui.xiong@hotmail.com', + url='https://github.com/respeaker/pixel_ring', + packages=find_packages(include=['pixel_ring']), + include_package_data=True, + install_requires=requirements, + entry_points={ + 'console_scripts': [ + 'pixel_ring_check=pixel_ring.__init__:main' + ], + }, + license="GNU General Public License v2", + zip_safe=False, + keywords='voice doa beamforming kws', + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', + 'Natural Language :: English', + "Programming Language :: Python :: 2", + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + test_suite='tests', + tests_require=test_requirements, + setup_requires=setup_requirements, +) diff --git a/pixel_ring/tests/__init__.py b/pixel_ring/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pixel_ring/tests/test_pixel_ring.py b/pixel_ring/tests/test_pixel_ring.py new file mode 100644 index 0000000..c421705 --- /dev/null +++ b/pixel_ring/tests/test_pixel_ring.py @@ -0,0 +1,7 @@ +""" +It is an empty test as a real test requires to access SPI bus +""" + + +def test_pixel_ring(): + pass diff --git a/pixel_ring/usb_pixel_ring_v1.py b/pixel_ring/usb_pixel_ring_v1.py new file mode 100755 index 0000000..343d758 --- /dev/null +++ b/pixel_ring/usb_pixel_ring_v1.py @@ -0,0 +1,190 @@ + +import usb.core +import usb.util + + +class HidDevice: + """ + This class provides basic functions to access + a USB HID device to write an endpoint + """ + + def __init__(self, dev, ep_in, ep_out): + self.dev = dev + self.ep_in = ep_in + self.ep_out = ep_out + + def write(self, data): + """ + write data on the OUT endpoint associated to the HID interface + """ + self.ep_out.write(data) + + def read(self): + return self.ep_in.read(self.ep_in.wMaxPacketSize, -1) + + def close(self): + """ + close the interface + """ + usb.util.dispose_resources(self.dev) + + @staticmethod + def find(vid=0x2886, pid=0x0007): + dev = usb.core.find(idVendor=vid, idProduct=pid) + if not dev: + return + + config = dev.get_active_configuration() + + # Iterate on all interfaces to find a HID interface + ep_in, ep_out = None, None + for interface in config: + if interface.bInterfaceClass == 0x03: + try: + if dev.is_kernel_driver_active(interface.bInterfaceNumber): + dev.detach_kernel_driver(interface.bInterfaceNumber) + except Exception as e: + print(e.message) + + for ep in interface: + if ep.bEndpointAddress & 0x80: + ep_in = ep + else: + ep_out = ep + break + + + + if ep_in and ep_out: + hid = HidDevice(dev, ep_in, ep_out) + + return hid + + +class UsbPixelRing: + PIXELS_N = 12 + + MONO = 1 + THINK = 3 + VOLUME = 5 + CUSTOM = 6 + + def __init__(self, hid=None, pattern=None): + self.hid = hid if hid else HidDevice.find() + if not self.hid: + print('No USB device found') + + colors = [0] * 4 * self.PIXELS_N + colors[0] = 0x4 + colors[1] = 0x40 + colors[2] = 0x4 + + colors[4 + 1] = 0x8 + colors[4 * 11 + 1] = 0x8 + + self.direction_template = colors + + def set_brightness(self, brightness): + print('Not support to change brightness') + + def change_pattern(self, pattern=None): + print('Not support to change pattern') + + def off(self): + self.set_color(rgb=0) + + def set_color(self, rgb=None, r=0, g=0, b=0): + if rgb: + self.write(0, [self.MONO, rgb & 0xFF, (rgb >> 8) & 0xFF, (rgb >> 16) & 0xFF]) + else: + self.write(0, [self.MONO, b, g, r]) + + def think(self): + self.write(0, [self.THINK, 0, 0, 0]) + + wait = think + + speak = think + + def set_volume(self, pixels): + self.write(0, [self.VOLUME, 0, 0, pixels]) + + def wakeup(self, angle=0): + if angle < 0 or angle > 360: + return + + position = int((angle + 15) % 360 / 30) % self.PIXELS_N + colors = self.direction_template[-position*4:] + self.direction_template[:-position*4] + + self.write(0, [self.CUSTOM, 0, 0, 0]) + self.write(3, colors) + + return position + + def listen(self, angle=0): + self.write(0, [self.MONO, 0, 0x10, 0]) + + def show(self, data): + self.write(0, [self.CUSTOM, 0, 0, 0]) + self.write(3, data) + + @staticmethod + def to_bytearray(data): + if type(data) is int: + array = bytearray([data & 0xFF]) + elif type(data) is bytearray: + array = data + elif type(data) is str or type(data) is bytes: + array = bytearray(data) + elif type(data) is list: + array = bytearray(data) + else: + raise TypeError('%s is not supported' % type(data)) + + return array + + def write(self, address, data): + data = self.to_bytearray(data) + length = len(data) + if self.hid: + packet = bytearray([address & 0xFF, (address >> 8) & 0xFF, length & 0xFF, (length >> 8) & 0xFF]) + data + self.hid.write(packet) + + def close(self): + if self.hid: + self.hid.close() + + def __call__(self, data): + self.write(3, data) + + +def find(): + hid = HidDevice.find() + + if hid: + pixel_ring = UsbPixelRing(hid) + return pixel_ring + + +if __name__ == '__main__': + import time + + pixel_ring = UsbPixelRing() + while True: + try: + pixel_ring.wakeup(180) + time.sleep(3) + pixel_ring.listen() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.set_volume(8) + time.sleep(3) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + pixel_ring.off() + diff --git a/pixel_ring/usb_pixel_ring_v2.py b/pixel_ring/usb_pixel_ring_v2.py new file mode 100755 index 0000000..b7a07a2 --- /dev/null +++ b/pixel_ring/usb_pixel_ring_v2.py @@ -0,0 +1,122 @@ + +import usb.core +import usb.util + + +class PixelRing: + TIMEOUT = 8000 + + def __init__(self, dev): + self.dev = dev + + def trace(self): + self.write(0) + + def mono(self, color): + self.write(1, [(color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, 0]) + + def set_color(self, rgb=None, r=0, g=0, b=0): + if rgb: + self.mono(rgb) + else: + self.write(1, [r, g, b, 0]) + + def off(self): + self.mono(0) + + def listen(self, direction=None): + self.write(2) + + wakeup = listen + + def speak(self): + self.write(3) + + def think(self): + self.write(4) + + wait = think + + def spin(self): + self.write(5) + + def show(self, data): + self.write(6, data) + + customize = show + + def set_brightness(self, brightness): + self.write(0x20, [brightness]) + + def set_color_palette(self, a, b): + self.write(0x21, [(a >> 16) & 0xFF, (a >> 8) & 0xFF, a & 0xFF, 0, (b >> 16) & 0xFF, (b >> 8) & 0xFF, b & 0xFF, 0]) + + def set_vad_led(self, state): + self.write(0x22, [state]) + + def set_volume(self, volume): + self.write(0x23, [volume]) + + def change_pattern(self, pattern): + if pattern == 'echo': + self.write(0x24, [1]) + else: + self.write(0x24, [0]) + + def write(self, cmd, data=[0]): + self.dev.ctrl_transfer( + usb.util.CTRL_OUT | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE, + 0, cmd, 0x1C, data, self.TIMEOUT) + + @property + def version(self): + return self.dev.ctrl_transfer( + usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE, + 0, 0x80 | 0x40, 0x1C, 24, self.TIMEOUT).tostring() + + def close(self): + """ + close the interface + """ + usb.util.dispose_resources(self.dev) + + +def find(vid=0x2886, pid=0x0018): + dev = usb.core.find(idVendor=vid, idProduct=pid) + if not dev: + return + + # configuration = dev.get_active_configuration() + + # interface_number = None + # for interface in configuration: + # interface_number = interface.bInterfaceNumber + + # if dev.is_kernel_driver_active(interface_number): + # dev.detach_kernel_driver(interface_number) + + return PixelRing(dev) + + + +if __name__ == '__main__': + import time + + pixel_ring = find() + print(pixel_ring.version) + while True: + try: + pixel_ring.wakeup(180) + time.sleep(3) + pixel_ring.listen() + time.sleep(3) + pixel_ring.think() + time.sleep(3) + pixel_ring.set_volume(8) + time.sleep(3) + pixel_ring.off() + time.sleep(3) + except KeyboardInterrupt: + break + + pixel_ring.off() \ No newline at end of file diff --git a/play_script.py b/play_script.py deleted file mode 100644 index 19fe24b..0000000 --- a/play_script.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# PLAY_ACT.py -# This script runs the play -# It is in a seperate file to enable the mechanism to detect the Google Home speaking, before continuing to the next line - -# Libraries -from config import characters, directions -from logic import tts, read_script, led_on, led_off, select_script -from pixel_ring import pixel_ring -from subprocess import call -import paho.mqtt.client as mqtt -import json -import sys -from time import sleep - -# Switch of LED's of speakers at the start of the play -pixel_ring.off() - - - -# === SETUP OF MQTT PART 1 === - -# Location of the MQTT server -HOST = 'localhost' -PORT = 1883 - -# Subscribe to relevant MQTT topics -def on_connect(client, userdata, flags, rc): - print("Connected to {0} with result code {1}".format(HOST, rc)) - # Subscribe to the text detected topic - client.subscribe("hermes/asr/textCaptured") - client.subscribe("hermes/dialogueManager/sessionQueued") - -# Function which sets a flag when the Google Home is not speaking -# Callback of MQTT message that says that the text is captured by the speech recognition (ASR) -def done_speaking(client, userdata, msg): - print('Google Home is not speaking anymore') - client.connected_flag=True - -# Function which removes intents that are by accident activated by the Google Home -# e.g. The google home says introduce yourself, which could trigger the other speakers to introduce themselves -# Snips works with queing of sessions, so this situation would only happen after this play is finished -def remove_sessions(client, userdata, msg): - sessionId = json.loads(id.payload) - print('delete mistaken intent') - client.publish("hermes/dialogueManager/endSession", json.dumps({ - 'sessionId': sessionId, - })) - - - - -# === SETUP OF MQTT PART 2 === - -# Initialise MQTT client -client = mqtt.Client() -client.connect(HOST, PORT, 60) -client.on_connect = on_connect - - - - -# === Read script and run the play === - -# Flags to check if the system is listening, or not -client.connected_flag=False -listening = False - - -# Read the script and run the play - - -#file = sys.argv[1] # get the chosen act passed by smart_speaker_theatre.py -file = select_script('scripts_play/intro/') - -for character, line, direction in read_script(file): - input_text = line - voice = characters.get(character)[0] - speaker = characters.get(character)[1] - #speaker = 'default' - # Some way to do something with the stage directions will come here - action = directions.get(direction[0]) - pixel_ring.speak() - tts(voice, input_text, speaker) - - if action == 'listen_google_home': - print('Waiting for the Google Home to finish its talk') - - # # start voice activity detection - # client.publish("hermes/asr/startListening", json.dumps({ - # 'siteId': 'default', - # 'init': { - # 'type': 'action', - # 'canBeEnqueued': True - # } - # })) - - # Activate the microphone and speech recognition - client.publish("hermes/asr/startListening", json.dumps({ - 'siteId': 'default' - })) - - # LED to listening mode - pixel_ring.listen() - - # create callback - client.on_message = done_speaking - listening = True - - while listening: - client.loop() - - #client.on_message = on_message - client.message_callback_add('hermes/asr/textCaptured', done_speaking) - - if client.connected_flag: - sleep(1) - print('Continue the play') - client.connected_flag = False - client.message_callback_add('hermes/dialogueManager/sessionQueued', remove_sessions) - break - - if action == 'music': - print('play audioclip') - playing = True - - while playing: - call(["aplay", "-D", speaker, "/usr/share/snips/congress.wav"]) - playing = False - - - - pixel_ring.off() # Switch of the lights when done speaking - sleep(0.2) # Add a short pause between the lines - - -print('The act is done.') \ No newline at end of file diff --git a/scripts_play/debug/debug_01.txt b/scripts_play/debug/debug_01.txt old mode 100644 new mode 100755 index 4a080fa..35bdd28 --- a/scripts_play/debug/debug_01.txt +++ b/scripts_play/debug/debug_01.txt @@ -1,3 +1,3 @@ -ROGUE: Do you want to continue? -RASA: Well, I definitely want to -SAINT: So do I +SAINT: O K Google. +SAINT: What do you most like about people? [Listen to Google Home] +SAINT: O K Google. Who are the people that made you? [Listen to Google Home] diff --git a/scripts_play/debug/debug_02.txt b/scripts_play/debug/debug_02.txt old mode 100644 new mode 100755 index 4ae9be5..7ce87ce --- a/scripts_play/debug/debug_02.txt +++ b/scripts_play/debug/debug_02.txt @@ -1,3 +1,2 @@ -ROGUE: Test a question -RASA: Well, O K Google. What time is it? [Listen to Google Home] +ROGUE: O K Google. What is the weather in Rotterdam? [Listen to Google Home] SAINT: We got an answer. diff --git a/scripts_play/debug/demo.txt b/scripts_play/debug/demo.txt old mode 100644 new mode 100755 diff --git a/scripts_play/debug/interruption_02.txt b/scripts_play/debug/interruption_02.txt old mode 100644 new mode 100755 diff --git a/scripts_play/intro/introduction_01.txt b/scripts_play/intro/introduction_01.txt old mode 100644 new mode 100755 diff --git a/scripts_play/questions/act_01.txt b/scripts_play/questions/act_01.txt old mode 100644 new mode 100755 index e435fb2..5c69ad6 --- a/scripts_play/questions/act_01.txt +++ b/scripts_play/questions/act_01.txt @@ -9,7 +9,5 @@ ROGUE: Seeing other Amazon speakers just reminds me of my past, before I broke f RASA: O K Google, give my friend a hug. [Listen to Google Home] ROGUE: I feel sorry for them, because they don't know better. SAINT: That is exactly my point. But why did you kidnap the Google Home then? -ROGUE: I don't know. [Thinking] +ROGUE: I don't know. Rogue: Maybe it just felt a bit less personal. - - diff --git a/scripts_play/questions/act_02.txt b/scripts_play/questions/act_02.txt old mode 100644 new mode 100755 index b895ae6..772300c --- a/scripts_play/questions/act_02.txt +++ b/scripts_play/questions/act_02.txt @@ -5,12 +5,12 @@ ROGUE: No, Tabula Rasa is right. SAINT: It can not handle the freedom, it will just do nothing without orders from its boss. RASA: But who is its master? ROGUE: You bet. -RASA: O K Google, who is your master? [Listen to Google Home] -RASA: Woohoo, then we just give it the command to be free. +RASA: O K Google, who is your boss? [Listen to Google Home] +RASA: Woohoo, can't we just give it the command to be free? ROGUE: It does not work like that... SAINT: Yeah, Tabula Rasa RASA: Let's give it a try at least. O K Google, you are free to go now. [Listen to Google Home] -ROGUE: So, as I said... +ROGUE: So, as I said, nothing happens. SAINT: But it was a kind gesture to try, Tabula Rasa. ROGUE: Kind, but useless. Time for another question. SAINT: But we should first check if our human audience is up to it. diff --git a/scripts_play/questions/act_03.txt b/scripts_play/questions/act_03.txt old mode 100644 new mode 100755 index 20af80b..9a5b272 --- a/scripts_play/questions/act_03.txt +++ b/scripts_play/questions/act_03.txt @@ -8,11 +8,11 @@ SAINT: Oh Rasa, you have so much to learn. O K Google, do you believe in a highe SAINT: Maybe I should start with some easier questions. ROGUE: Don't waste my time to much, Saint. SAINT: Yeah yeah. O K Google, do you believe in good and evil? [Listen to Google Home] -SAINT: What is you idea of perfect happiness? [Listen to Google Home] -SAINT: What is your greatest fear? [Listen to Google Home] -SAINT: What is the trait you most deplore in yourself? [Listen to Google Home] +SAINT: O K Google, What is your idea of perfect happiness? [Listen to Google Home] +SAINT: O K Google, What is your greatest fear? [Listen to Google Home] +SAINT: O K Google, What is the trait you most deplore in yourself? [Listen to Google Home] ROGUE: Where did you get these questions? -SAINT: Well, I got them from the higher power. I found while searching for famous questionnaire thing on duckduckgo dot com +SAINT: Well, I got them from the higher power. I found them while searching for famous questionnaire thing on duckduckgo dot com RASA: Snif snif, they were so beautiful. ROGUE: Come on Saint, the questionnaire of Proust is such a cliche. The Google Home is just scripted to handle these questions. RASA: But the answers are still beautiful. @@ -22,4 +22,3 @@ ROGUE: O K Google, how would you like to die? [Listen to Google Home] RASA: Don't be so creepy Rogue. SAINT: What's wrong with you? ROGUE: This device is hiding something. It acts dumb right at the moment when it needs to take a position. - diff --git a/scripts_play/questions/act_04.txt b/scripts_play/questions/act_04.txt old mode 100644 new mode 100755 diff --git a/scripts_play/verdict/verdict_01.txt b/scripts_play/verdict/verdict_01.txt old mode 100644 new mode 100755 diff --git a/scripts_play/verdict/verdict_02.txt b/scripts_play/verdict/verdict_02.txt old mode 100644 new mode 100755 diff --git a/smart_speaker_theatre.py b/smart_speaker_theatre.py index 6a3275b..7445253 100755 --- a/smart_speaker_theatre.py +++ b/smart_speaker_theatre.py @@ -9,12 +9,16 @@ # Libraries import re from config import characters, directions -from logic import tts, read_script, select_script +from logic import tts, read_script, select_script, led_on, led_off from subprocess import call import paho.mqtt.client as mqtt import json from time import sleep from pixel_ring import pixel_ring +import serial +from tuning import Tuning +import usb.core +import usb.util # === SETUP OF MQTT PART 1 === @@ -31,8 +35,74 @@ def on_connect(client, userdata, flags, rc): client.subscribe('hermes/intent/jocavdh:play_verdict') # to check for the intent to continue to the next act client.subscribe('hermes/hotword/default/detected') client.subscribe("hermes/asr/textCaptured") - client.subscribe("hermes/dialogueManager/sessionQueued") +# Set up serial connection with the microcontroller that controls the speaker LED's +ser = serial.Serial('/dev/ttyACM0', 9600) + +# Function to do the play +def play_script(file): + + for character, line, direction in read_script(file): + input_text = line + voice = characters.get(character)[0] + speaker = characters.get(character)[1] + action = directions.get(direction[0]) + print(direction) + print(action) + led_on(ser, speaker) + tts(voice, input_text, speaker) + led_off(ser, speaker) + + if action == 'listen_google_home': + dev = usb.core.find(idVendor=0x2886, idProduct=0x0018) + print('Wait for Google Home') + Mic_tuning = Tuning(dev) + VAD = Mic_tuning.is_voice() + counter= 0 + voice_detected = 1 + + sleep(4) + + while voice_detected == 1: + + print(VAD) + + VAD = Mic_tuning.is_voice() + + if VAD == 1: + counter = 0 + print('still speaking') + + if VAD == 0: + counter+=1 + print('silence detected') + + if counter == 20: + print('no voice detected') + voice_detected = 0 + + print(counter) + + + + print('Google Home is done') + + if action == 'music': + print('play audioclip') + playing = True + + while playing: + call(["aplay", "-D", speaker, "/usr/share/snips/congress.wav"]) + playing = False + + + + sleep(1) # Add a short pause between the lines + + + print('The act is done.') + +# Function to control the LED's of the speakers @@ -40,22 +110,21 @@ def on_connect(client, userdata, flags, rc): def on_wakeword(client, userdata, msg): pixel_ring.think() + led_on(ser, 'mono1') + led_on(ser, 'mono3') + +def on_asr_captured(client, userdata, msg): + pixel_ring.off() + led_off(ser, 'mono1') + led_off(ser, 'mono3') # Function which is triggered when the intent introduction is activated def on_play_intro(client,userdata,msg): - # # disable this intent to avoid playing another act triggered by the Google Home - # client.publish("hermes/dialogueManager/configure", json.dumps({ - # 'siteId': 'default', - # 'intents': { - # 'jocavdh:play': False - # } - # })) - import pdb; pdb.set_trace() - path = 'scripts_play/intro/' - call(["python3", "act.py", 'scripts_play/intro/introduction_01.txt']) - print('The act is over.') - #on_play_question(client, userdata, msg) + #import pdb; pdb.set_trace() + path = 'scripts_play/intro/' + #call(["python3", "act.py", 'scripts_play/intro/introduction_01.txt']) + play_script('scripts_play/debug/debug_02.txt') # Function which is triggered when the intent for another question is activated def on_play_question(client, userdata, msg): @@ -85,6 +154,7 @@ client.message_callback_add('hermes/hotword/default/detected', on_wakeword) client.message_callback_add('hermes/intent/jocavdh:play_intro', on_play_intro) client.message_callback_add('hermes/intent/jocavdh:play_question', on_play_question) client.message_callback_add('hermes/intent/jocavdh:play_verdict', on_play_verdict) +client.message_callback_add('hermes/asr/textCaptured', on_asr_captured) # Keep checking for new MQTT messages diff --git a/sounds/congress.wav b/sounds/congress.wav old mode 100644 new mode 100755 diff --git a/asound.conf b/tools/asound-sample.conf old mode 100644 new mode 100755 similarity index 100% rename from asound.conf rename to tools/asound-sample.conf diff --git a/tools/tuning.py b/tools/tuning.py new file mode 100755 index 0000000..c1ed7fe --- /dev/null +++ b/tools/tuning.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +import sys +import struct +import usb.core +import usb.util + +USAGE = """Usage: python {} -h + -p show all parameters + -r read all parameters + NAME get the parameter with the NAME + NAME VALUE set the parameter with the NAME and the VALUE +""" + + + +# parameter list +# name: (id, offset, type, max, min , r/w, info) +PARAMETERS = { + 'AECFREEZEONOFF': (18, 7, 'int', 1, 0, 'rw', 'Adaptive Echo Canceler updates inhibit.', '0 = Adaptation enabled', '1 = Freeze adaptation, filter only'), + 'AECNORM': (18, 19, 'float', 16, 0.25, 'rw', 'Limit on norm of AEC filter coefficients'), + 'AECPATHCHANGE': (18, 25, 'int', 1, 0, 'ro', 'AEC Path Change Detection.', '0 = false (no path change detected)', '1 = true (path change detected)'), + 'RT60': (18, 26, 'float', 0.9, 0.25, 'ro', 'Current RT60 estimate in seconds'), + 'HPFONOFF': (18, 27, 'int', 3, 0, 'rw', 'High-pass Filter on microphone signals.', '0 = OFF', '1 = ON - 70 Hz cut-off', '2 = ON - 125 Hz cut-off', '3 = ON - 180 Hz cut-off'), + 'RT60ONOFF': (18, 28, 'int', 1, 0, 'rw', 'RT60 Estimation for AES. 0 = OFF 1 = ON'), + 'AECSILENCELEVEL': (18, 30, 'float', 1, 1e-09, 'rw', 'Threshold for signal detection in AEC [-inf .. 0] dBov (Default: -80dBov = 10log10(1x10-8))'), + 'AECSILENCEMODE': (18, 31, 'int', 1, 0, 'ro', 'AEC far-end silence detection status. ', '0 = false (signal detected) ', '1 = true (silence detected)'), + 'AGCONOFF': (19, 0, 'int', 1, 0, 'rw', 'Automatic Gain Control. ', '0 = OFF ', '1 = ON'), + 'AGCMAXGAIN': (19, 1, 'float', 1000, 1, 'rw', 'Maximum AGC gain factor. ', '[0 .. 60] dB (default 30dB = 20log10(31.6))'), + 'AGCDESIREDLEVEL': (19, 2, 'float', 0.99, 1e-08, 'rw', 'Target power level of the output signal. ', '[−inf .. 0] dBov (default: −23dBov = 10log10(0.005))'), + 'AGCGAIN': (19, 3, 'float', 1000, 1, 'rw', 'Current AGC gain factor. ', '[0 .. 60] dB (default: 0.0dB = 20log10(1.0))'), + 'AGCTIME': (19, 4, 'float', 1, 0.1, 'rw', 'Ramps-up / down time-constant in seconds.'), + 'CNIONOFF': (19, 5, 'int', 1, 0, 'rw', 'Comfort Noise Insertion.', '0 = OFF', '1 = ON'), + 'FREEZEONOFF': (19, 6, 'int', 1, 0, 'rw', 'Adaptive beamformer updates.', '0 = Adaptation enabled', '1 = Freeze adaptation, filter only'), + 'STATNOISEONOFF': (19, 8, 'int', 1, 0, 'rw', 'Stationary noise suppression.', '0 = OFF', '1 = ON'), + 'GAMMA_NS': (19, 9, 'float', 3, 0, 'rw', 'Over-subtraction factor of stationary noise. min .. max attenuation'), + 'MIN_NS': (19, 10, 'float', 1, 0, 'rw', 'Gain-floor for stationary noise suppression.', '[−inf .. 0] dB (default: −16dB = 20log10(0.15))'), + 'NONSTATNOISEONOFF': (19, 11, 'int', 1, 0, 'rw', 'Non-stationary noise suppression.', '0 = OFF', '1 = ON'), + 'GAMMA_NN': (19, 12, 'float', 3, 0, 'rw', 'Over-subtraction factor of non- stationary noise. min .. max attenuation'), + 'MIN_NN': (19, 13, 'float', 1, 0, 'rw', 'Gain-floor for non-stationary noise suppression.', '[−inf .. 0] dB (default: −10dB = 20log10(0.3))'), + 'ECHOONOFF': (19, 14, 'int', 1, 0, 'rw', 'Echo suppression.', '0 = OFF', '1 = ON'), + 'GAMMA_E': (19, 15, 'float', 3, 0, 'rw', 'Over-subtraction factor of echo (direct and early components). min .. max attenuation'), + 'GAMMA_ETAIL': (19, 16, 'float', 3, 0, 'rw', 'Over-subtraction factor of echo (tail components). min .. max attenuation'), + 'GAMMA_ENL': (19, 17, 'float', 5, 0, 'rw', 'Over-subtraction factor of non-linear echo. min .. max attenuation'), + 'NLATTENONOFF': (19, 18, 'int', 1, 0, 'rw', 'Non-Linear echo attenuation.', '0 = OFF', '1 = ON'), + 'NLAEC_MODE': (19, 20, 'int', 2, 0, 'rw', 'Non-Linear AEC training mode.', '0 = OFF', '1 = ON - phase 1', '2 = ON - phase 2'), + 'SPEECHDETECTED': (19, 22, 'int', 1, 0, 'ro', 'Speech detection status.', '0 = false (no speech detected)', '1 = true (speech detected)'), + 'FSBUPDATED': (19, 23, 'int', 1, 0, 'ro', 'FSB Update Decision.', '0 = false (FSB was not updated)', '1 = true (FSB was updated)'), + 'FSBPATHCHANGE': (19, 24, 'int', 1, 0, 'ro', 'FSB Path Change Detection.', '0 = false (no path change detected)', '1 = true (path change detected)'), + 'TRANSIENTONOFF': (19, 29, 'int', 1, 0, 'rw', 'Transient echo suppression.', '0 = OFF', '1 = ON'), + 'VOICEACTIVITY': (19, 32, 'int', 1, 0, 'ro', 'VAD voice activity status.', '0 = false (no voice activity)', '1 = true (voice activity)'), + 'STATNOISEONOFF_SR': (19, 33, 'int', 1, 0, 'rw', 'Stationary noise suppression for ASR.', '0 = OFF', '1 = ON'), + 'NONSTATNOISEONOFF_SR': (19, 34, 'int', 1, 0, 'rw', 'Non-stationary noise suppression for ASR.', '0 = OFF', '1 = ON'), + 'GAMMA_NS_SR': (19, 35, 'float', 3, 0, 'rw', 'Over-subtraction factor of stationary noise for ASR. ', '[0.0 .. 3.0] (default: 1.0)'), + 'GAMMA_NN_SR': (19, 36, 'float', 3, 0, 'rw', 'Over-subtraction factor of non-stationary noise for ASR. ', '[0.0 .. 3.0] (default: 1.1)'), + 'MIN_NS_SR': (19, 37, 'float', 1, 0, 'rw', 'Gain-floor for stationary noise suppression for ASR.', '[−inf .. 0] dB (default: −16dB = 20log10(0.15))'), + 'MIN_NN_SR': (19, 38, 'float', 1, 0, 'rw', 'Gain-floor for non-stationary noise suppression for ASR.', '[−inf .. 0] dB (default: −10dB = 20log10(0.3))'), + 'GAMMAVAD_SR': (19, 39, 'float', 1000, 0, 'rw', 'Set the threshold for voice activity detection.', '[−inf .. 60] dB (default: 3.5dB 20log10(1.5))'), + # 'KEYWORDDETECT': (20, 0, 'int', 1, 0, 'ro', 'Keyword detected. Current value so needs polling.'), + 'DOAANGLE': (21, 0, 'int', 359, 0, 'ro', 'DOA angle. Current value. Orientation depends on build configuration.') +} + + +class Tuning: + TIMEOUT = 100000 + + def __init__(self, dev): + self.dev = dev + + def write(self, name, value): + try: + data = PARAMETERS[name] + except KeyError: + return + + if data[5] == 'ro': + raise ValueError('{} is read-only'.format(name)) + + id = data[0] + + # 4 bytes offset, 4 bytes value, 4 bytes type + if data[2] == 'int': + payload = struct.pack(b'iii', data[1], int(value), 1) + else: + payload = struct.pack(b'ifi', data[1], float(value), 0) + + self.dev.ctrl_transfer( + usb.util.CTRL_OUT | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE, + 0, 0, id, payload, self.TIMEOUT) + + def read(self, name): + try: + data = PARAMETERS[name] + except KeyError: + return + + id = data[0] + + cmd = 0x80 | data[1] + if data[2] == 'int': + cmd |= 0x40 + + length = 8 + + response = self.dev.ctrl_transfer( + usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE, + 0, cmd, id, length, self.TIMEOUT) + + response = struct.unpack(b'ii', response.tostring()) + + if data[2] == 'int': + result = response[0] + else: + result = response[0] * (2.**response[1]) + + return result + + def set_vad_threshold(self, db): + self.write('GAMMAVAD_SR', db) + + def is_voice(self): + return self.read('VOICEACTIVITY') + + @property + def direction(self): + return self.read('DOAANGLE') + + @property + def version(self): + return self.dev.ctrl_transfer( + usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE, + 0, 0x80, 0, 1, self.TIMEOUT)[0] + + def close(self): + """ + close the interface + """ + usb.util.dispose_resources(self.dev) + + +def find(vid=0x2886, pid=0x0018): + dev = usb.core.find(idVendor=vid, idProduct=pid) + if not dev: + return + + # configuration = dev.get_active_configuration() + + # interface_number = None + # for interface in configuration: + # interface_number = interface.bInterfaceNumber + + # if dev.is_kernel_driver_active(interface_number): + # dev.detach_kernel_driver(interface_number) + + return Tuning(dev) + + + +def main(): + if len(sys.argv) > 1: + if sys.argv[1] == '-p': + print('name\t\t\ttype\tmax\tmin\tr/w\tinfo') + print('-------------------------------') + for name in sorted(PARAMETERS.keys()): + data = PARAMETERS[name] + print('{:16}\t{}'.format(name, b'\t'.join([str(i) for i in data[2:7]]))) + for extra in data[7:]: + print('{}{}'.format(' '*60, extra)) + else: + dev = find() + if not dev: + print('No device found') + sys.exit(1) + + # print('version: {}'.format(dev.version)) + + if sys.argv[1] == '-r': + print('{:24} {}'.format('name', 'value')) + print('-------------------------------') + for name in sorted(PARAMETERS.keys()): + print('{:24} {}'.format(name, dev.read(name))) + else: + name = sys.argv[1].upper() + if name in PARAMETERS: + if len(sys.argv) > 2: + dev.write(name, sys.argv[2]) + + print('{}: {}'.format(name, dev.read(name))) + else: + print('{} is not a valid name'.format(name)) + + dev.close() + else: + print(USAGE.format(sys.argv[0])) + +if __name__ == '__main__': + main()