mycroft-core/mycroft/client/enclosure/mouth.py

270 lines
9.8 KiB
Python

# 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)