# Copyright 2017 Mycroft AI Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import time from PIL import Image class EnclosureMouth: """ Listens to enclosure commands for Mycroft's Mouth. Performs the associated command on Arduino by writing on the Serial port. """ def __init__(self, bus, writer): self.bus = bus self.writer = writer self.is_timer_on = False self.__init_events() def __init_events(self): self.bus.on('enclosure.mouth.reset', self.reset) self.bus.on('enclosure.mouth.talk', self.talk) self.bus.on('enclosure.mouth.think', self.think) self.bus.on('enclosure.mouth.listen', self.listen) self.bus.on('enclosure.mouth.smile', self.smile) self.bus.on('enclosure.mouth.viseme', self.viseme) self.bus.on('enclosure.mouth.text', self.text) self.bus.on('enclosure.mouth.display', self.display) self.bus.on('enclosure.mouth.display_image', self.display_image) self.bus.on('enclosure.weather.display', self.display_weather) def reset(self, event=None): self.writer.write("mouth.reset") def talk(self, event=None): self.writer.write("mouth.talk") def think(self, event=None): self.writer.write("mouth.think") def listen(self, event=None): self.writer.write("mouth.listen") def smile(self, event=None): self.writer.write("mouth.smile") def viseme(self, event=None): if event and event.data: code = event.data.get("code") time_until = event.data.get("until") # Skip the viseme if the time has expired. This helps when a # system glitch overloads the bus and throws off the timing of # the animation timing. if code and (not time_until or time.time() < time_until): self.writer.write("mouth.viseme=" + code) def text(self, event=None): text = "" if event and event.data: text = event.data.get("text", text) self.writer.write("mouth.text=" + text) def __display(self, code, clear_previous, x_offset, y_offset): """ Write the encoded image to enclosure screen. Arguments: code (str): encoded image to display clean_previous (str): if "True" will clear the screen before drawing. x_offset (int): x direction offset y_offset (int): y direction offset """ clear_previous = int(str(clear_previous) == "True") clear_previous = "cP=" + str(clear_previous) + "," x_offset = "x=" + str(x_offset) + "," y_offset = "y=" + str(y_offset) + "," message = "mouth.icon=" + x_offset + y_offset + clear_previous + code # Check if message exceeds Arduino's serial buffer input limit 64 bytes if len(message) > 60: message1 = message[:31] message2 = message[31:] message1 += "$" message2 += "$" message2 = "mouth.icon=" + message2 self.writer.write(message1) time.sleep(0.25) # writer bugs out if sending messages too rapidly self.writer.write(message2) else: time.sleep(0.1) self.writer.write(message) def display(self, event=None): """ Display a Mark-1 specific code. Arguments: event (Message): messagebus message with data to display """ code = "" x_offset = "" y_offset = "" clear_previous = "" if event and event.data: code = event.data.get("img_code", code) x_offset = int(event.data.get("xOffset", xOffset)) y_offset = int(event.data.get("yOffset", yOffset)) clear_previous = event.data.get("clearPrev", clearPrevious) self.__display(code, clear_previous, x_offset, y_offset) def display_image(self, event=None): """ Display an image on the enclosure. The method uses PIL to convert the image supplied into a code suitable for the Mark-1 display. Arguments: event (Message): messagebus message with data to display """ if not event: return image_absolute_path = event.data['img_path'] refresh = event.data['clearPrev'] invert = event.data['invert'] x_offset = event.data['xOffset'] y_offset = event.data['yOffset'] threshold = event.data.get('threshold', 70) # default threshold # to understand how this funtion works you need to understand how the # Mark I arduino proprietary encoding works to display to the faceplate img = Image.open(image_absolute_path).convert("RGBA") img2 = Image.new('RGBA', img.size, (255, 255, 255)) width = img.size[0] height = img.size[1] # strips out alpha value and blends it with the RGB values img = Image.alpha_composite(img2, img) img = img.convert("L") # crop image to only allow a max width of 16 if width > 32: img = img.crop((0, 0, 32, height)) width = img.size[0] height = img.size[1] # crop the image to limit the max height of 8 if height > 8: img = img.crop((0, 0, width, 8)) width = img.size[0] height = img.size[1] encode = "" # Each char value represents a width number starting with B=1 # then increment 1 for the next. ie C=2 width_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a'] height_codes = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] encode += width_codes[width - 1] encode += height_codes[height - 1] # Turn the image pixels into binary values 1's and 0's # the Mark I face plate encoding uses binary values to # binary_values returns a list of 1's and 0s'. ie ['1', '1', '0', ...] binary_values = [] for i in range(width): for j in range(height): if img.getpixel((i, j)) < threshold: if invert is False: binary_values.append('1') else: binary_values.append('0') else: if invert is False: binary_values.append('0') else: binary_values.append('1') # these values are used to determine how binary values # needs to be grouped together number_of_top_pixel = 0 number_of_bottom_pixel = 0 if height > 4: number_of_top_pixel = 4 number_of_bottom_pixel = height - 4 else: number_of_top_pixel = height # this loop will group together the individual binary values # ie. binary_list = ['1111', '001', '0101', '100'] binary_list = [] binary_code = '' increment = 0 alternate = False for val in binary_values: binary_code += val increment += 1 if increment == number_of_top_pixel and alternate is False: # binary code is reversed for encoding binary_list.append(binary_code[::-1]) increment = 0 binary_code = '' alternate = True elif increment == number_of_bottom_pixel and alternate is True: binary_list.append(binary_code[::-1]) increment = 0 binary_code = '' alternate = False # Code to let the Makrk I arduino know where to place the # pixels on the faceplate pixel_codes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'] for binary_values in binary_list: number = int(binary_values, 2) pixel_code = pixel_codes[number] encode += pixel_code self.__display(encode, refresh, x_offset, y_offset) def display_weather(self, event=None): if event and event.data: # Convert img_code to icon img_code = event.data.get("img_code", None) icon = None if img_code == 0: # sunny icon = "IICEIBMDNLMDIBCEAA" elif img_code == 1: # partly cloudy icon = "IIEEGBGDHLHDHBGEEA" elif img_code == 2: # cloudy icon = "IIIBMDMDODODODMDIB" elif img_code == 3: # light rain icon = "IIMAOJOFPBPJPFOBMA" elif img_code == 4: # raining icon = "IIMIOFOBPFPDPJOFMA" elif img_code == 5: # storming icon = "IIAAIIMEODLBJAAAAA" elif img_code == 6: # snowing icon = "IIJEKCMBPHMBKCJEAA" elif img_code == 7: # wind/mist icon = "IIABIBIBIJIJJGJAGA" temp = event.data.get("temp", None) if icon is not None and temp is not None: icon = "x=2," + icon msg = "weather.display=" + str(temp) + "," + str(icon) self.writer.write(msg)