Better LED-interface (to be tested)

This commit is contained in:
Remy Moll 2021-02-23 15:35:31 +01:00
parent b2007f2111
commit c61ee3ea72
16 changed files with 456 additions and 527 deletions

View File

@ -103,15 +103,16 @@ class Clock(BotFunc):
self.clock.set_brightness(value=1)
start_color = numpy.array([153, 0, 51])
end_color = numpy.array([255, 255, 0])
empty = numpy.zeros((16,32))
ones = empty
ones[ones == 0] = 1
col_show = numpy.zeros((*self.clock.shape, 3))
col_show[:,:,...] = start_color
gradient = end_color - start_color
# 20 steps should be fine => sleep_time = duration / 20
for i in range(20):
ct = i/20 * gradient
col = [int(x) for x in ct+start_color]
self.clock.IO.set_matrix(ones,colors=[col])
col_show[:,:,...] = [int(x) for x in ct+start_color]
self.clock.IO.array = col_show
self.clock.IO.SHOW()
time.sleep(int(duration) / 20)
self.clock.run(output,(duration,))
@ -123,17 +124,19 @@ class Clock(BotFunc):
frequency = update.message.text
def output(duration, frequency):
self.set_brightness(value=1)
self.clock.set_brightness(value=1)
duration = int(duration)
frequency = int(frequency)
n = duration * frequency / 2
empty = numpy.zeros((16,32))
empty = numpy.zeros((*self.clock.shape,3))
red = empty.copy()
red[red == 0] = 3
red[...,0] = 255
for i in range(int(n)):
self.IO.set_matrix(red)
self.clock.IO.array = red
self.clock.IO.SHOW()
time.sleep(1/frequency)
self.IO.set_matrix(empty)
self.clock.IO.array = empty
self.clock.IO.SHOW()
time.sleep(1/frequency)
if not(duration == 0 or frequency == 0):
@ -151,8 +154,8 @@ class Clock(BotFunc):
id = img.file_id
file = bot.getFile(id).download_as_bytearray()
width = self.clock.IO.width
height = self.clock.IO.height
width = self.clock.shape[1]
height = self.clock.shape[0]
img = Image.open(io.BytesIO(file))
im_height = img.height
@ -166,7 +169,8 @@ class Clock(BotFunc):
a = numpy.asarray(t)
def output(image, duration):
self.clock.IO.set_matrix(image)
self.clock.IO.array = image
self.clock.IO.SHOW()
time.sleep(int(duration) * 60)
self.clock.run(output,(a, duration))
@ -176,7 +180,7 @@ class Clock(BotFunc):
def exec_show_message(self, update: Update, context: CallbackContext) -> None:
message_str = update.message.text
update.message.reply_text("Now showing: " + message_str)
self.clock.run(self.clock.IO.text_scroll,(message_str, self.clock.tspeed, [200,200,200]))
self.clock.run(self.clock.text_scroll,(message_str,))
return ConversationHandler.END

View File

@ -1 +1,2 @@
# Placeholder
from . import led

View File

@ -1,130 +0,0 @@
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import datetime
"""Two colors: 1 main color and 1 accent color. These are labeled in the matrix as 1 and 2"""
def text_converter(text, height):
"""Converts a text to a pixel-matrix
returns: np.array((16, x))"""
font = ImageFont.truetype("verdanab.ttf", height)
size = font.getsize(text)
img = Image.new("1",size,"black")
draw = ImageDraw.Draw(img)
draw.text((0, 0), text, "white", font=font)
pixels = np.array(img, dtype=np.uint8)
return pixels
digits = {
"1" : [[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]],
"2" : [[1,1,1],[0,0,1],[1,1,1],[1,0,0],[1,1,1]],
"3" : [[1,1,1],[0,0,1],[1,1,1],[0,0,1],[1,1,1]],
"4" : [[1,0,1],[1,0,1],[1,1,1],[0,0,1],[0,0,1]],
"5" : [[1,1,1],[1,0,0],[1,1,1],[0,0,1],[1,1,1]],
"6" : [[1,1,1],[1,0,0],[1,1,1],[1,0,1],[1,1,1]],
"7" : [[1,1,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]],
"8" : [[1,1,1],[1,0,1],[1,1,1],[1,0,1],[1,1,1]],
"9" : [[1,1,1],[1,0,1],[1,1,1],[0,0,1],[1,1,1]],
"0" : [[1,1,1],[1,0,1],[1,0,1],[1,0,1],[1,1,1]],
"-" : [[0,0,0],[0,0,0],[1,1,1],[0,0,0],[0,0,0]],
"-1" : [[0,0,1],[0,0,1],[1,1,1],[0,0,1],[0,0,1]],
"error" : [[1,0,1],[1,0,1],[0,1,0],[1,0,1],[1,0,1]],
}
##place of numbers, invariant
digit_position = [[2,4], [2,10], [9,4], [9,10]]
def time_converter(top="", bottom=""):
"""Converts 4-digit time to a pixel-matrix
returns: np.array((16, 16))"""
pixels = np.zeros((16,16),dtype=np.uint8)
if bottom == "" or top == "":
top = datetime.datetime.now().strftime("%H")
bottom = datetime.datetime.now().strftime("%M")
if len(top) < 2:
top = "0" * (2 - len(top)) + top
if len(bottom) < 2:
bottom = "0" * (2 - len(bottom)) + bottom
if ("-" in top and len(top) > 2) or ("-" in bottom and len(bottom) > 2):
time_split = 4*["-"]
elif "error" in top and "error" in bottom:
time_split = 4*["error"]
else:
time_split = [i for i in top] + [i for i in bottom]
if "-1" in top and len(top) != 2:
time_split = ["-1", top[-1]] + [i for i in bottom]
if "-1" in bottom and len(bottom) != 2:
time_split = [i for i in top] + ["-1", bottom[-1]]
for i in range(4):
x = digit_position[i][0]
y = digit_position[i][1]
number = digits[time_split[i]]
pixels[x: x + 5, y: y + 3] = np.array(number)
return pixels
days = np.append(np.zeros((15,16)), np.array([0,1,0,1,0,1,0,1,0,1,0,1,1,0,1,1])).reshape((16,16))
def date_converter():
today = datetime.datetime.today()
weekday = datetime.datetime.weekday(today)
# size of the reduced array according to weekday
size = [2,4,6,8,10,13,16]
pixels = days.copy() #base color background
lrow = np.append(pixels[15,:size[weekday]], [0 for i in range(16 - size[weekday])])
lrow = np.append(np.zeros((15,16)), lrow).reshape((16,16))
pixels += lrow
return pixels
weather_categories = {
"Clouds": "cloud",
"Rain": "rain and cloud",
"Thunderstorm": "thunder and cloud",
"Drizzle": "rain and cloud",
"Snow": "snow and cloud",
"Clear": "sun",
"Mist": "fog and clouds",
"Smoke": "Smoke",
"Haze": "Haze",
"Dust": "Dust",
"Fog": "fog",
"Sand": "Sand",
"Dust": "Dust",
"Ash": "Ash",
"Squal": "Squal",
"Tornado": "Tornado",
"error" : "moon"
}
def weather_converter(name):
result = np.zeros((16,16))
cwd = __file__.replace("\\","/") # for windows
cwd = cwd.rsplit("/", 1)[0] # the current working directory (where this file is)
if len(cwd) == 0:
cwd = "."
icon_spritesheet = cwd + "/weather-icons.bmp"
icons = Image.open(icon_spritesheet)
icons_full = np.array(icons)
icon_loc = ["sun","moon","sun and clouds", "moon and clouds", "cloud","fog and clouds","2 clouds", "3 clouds", "rain and cloud", "rain and clouds", "rain and cloud and sun", "rain and cloud and moon", "thunder and cloud", "thunder and cloud and moon", "snow and cloud", "snow and cloud and moon", "fog","fog night"]
#ordered 1 2 \n 3 4 \ 5 5 ...
name = weather_categories[name]
try:
iy, ix = int(icon_loc.index(name)/2), icon_loc.index(name)%2
# x and y coords
except:
return np.zeros((16,16,3))
icon_single = icons_full[16*iy:16*(iy + 1),16*ix:16*(ix + 1),...]
return icon_single

View File

@ -1 +0,0 @@
# Placeholder

View File

@ -1,93 +0,0 @@
import sys
import colorsys
import pygame.gfxdraw
import time
import pygame
import numpy
class UnicornHat(object):
def __init__(self, width, height):
# Compat with old library
# Set some defaults
self.rotation(0)
self.pixel_size = 20
self.height = height
self.width = width
self.pixels = numpy.zeros((self.height,self.width,3), dtype=int)
self.window_width = self.width * self.pixel_size
self.window_height = self.height * self.pixel_size
self.brightness = 1
# Init pygame and off we go
pygame.init()
pygame.display.set_caption("Unicorn HAT simulator")
self.screen = pygame.display.set_mode([self.window_width, self.window_height])
self.clear()
def set_pixel(self, x, y, r, g, b):
self.pixels[x][y] = r, g, b
def set_matrix(self, matrix):
self.pixels = matrix
self.show()
def draw(self):
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT:
print("Exiting...")
sys.exit()
for i in range(self.height):
for j in range(self.width):
self.draw_led(i,j)
def draw_led(self,i, j):
p = self.pixel_size
w_x = int(j * p + p / 2)
#w_y = int((self.height - 1 - y) * p + p / 2)
w_y = int(i * p + p / 2)
r = int(p / 4)
color = self.pixels[i,j,:]*self.brightness
color = color.astype("int")
pygame.gfxdraw.aacircle(self.screen, w_x, w_y, r, color)
pygame.gfxdraw.filled_circle(self.screen, w_x, w_y, r, color)
def show(self):
self.clear()
self.draw()
pygame.display.flip()
pygame.event.pump()
#time.sleep(5)
def get_shape(self):
return (self.width, self.height)
def set_brightness(self, brightness):
self.brightness = brightness
def rotation(self, r):
self._rotation = int(round(r/90.0)) % 3
def clear(self):
self.screen.fill((0, 0, 0))
def get_rotation(self):
return self._rotation * 90
def off(self):
print("Closing window")
pygame.quit()

View File

@ -1,13 +0,0 @@
import unicorn
led = unicorn.UnicornHat(32,16)
r = 0
b = 0
for i in range(16*32):
x = i % 32
y = i // 32
r += (x % 16 == 0)*5
b+= (y % 16 == 0)*5
led.set_pixel(x,y, r, 200, b)
if i%2 == 0:
led.show()

View File

@ -1,151 +0,0 @@
import colorsys
import time
import numpy
import RPi.GPIO as GPIO
class UnicornHat(object):
def __init__(self, width, height, rotation_offset = 0):
self.PIN_CLK = 11
##################################
# GPIO Pins for the actual signal. The other ones are for signal clocks and resets.
self.PINS_DAT = [10, 22]
##################################
self.PIN_CS = 8
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(self.PIN_CS, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(self.PIN_CLK, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.PINS_DAT, GPIO.OUT, initial=GPIO.LOW)
self.SOF = 0x72
self.DELAY = 1.0/120
self.WIDTH = width #32
self.HEIGHT = height #16
self.rotation = 1
self.brightness = 1
self.buffer = numpy.zeros((self.HEIGHT,self.WIDTH,3), dtype=int)
self.reset_clock()
def reset_clock(self):
GPIO.output(self.PIN_CS, GPIO.LOW)
time.sleep(0.00001)
GPIO.output(self.PIN_CS, GPIO.HIGH)
def spi_write(self, buf1, buf2):
GPIO.output(self.PIN_CS, GPIO.LOW)
self.spi_write_byte(self.SOF, self.SOF)
for x in range(len(buf1)):
b1, b2= buf1[x], buf2[x]
self.spi_write_byte(b1, b2)
GPIO.output(self.PIN_CS, GPIO.HIGH)
def spi_write_byte(self, b1, b2):
for x in range(8):
GPIO.output(self.PINS_DAT[0], b1 & 0b10000000)
GPIO.output(self.PINS_DAT[1], b2 & 0b10000000)
GPIO.output(self.PIN_CLK, GPIO.HIGH)
b1 <<= 1
b2 <<= 1
#time.sleep(0.00000001)
GPIO.output(self.PIN_CLK, GPIO.LOW)
def set_brightness(self, b):
"""Set the display brightness between 0.0 and 1.0.
:param b: Brightness from 0.0 to 1.0 (default 1)
"""
self.brightness = b
def rotation(self, r):
"""Set the display rotation in degrees.
Actual rotation will be snapped to the nearest 90 degrees.
"""
self.rotation = int(round(r/90.0))
def get_rotation(self):
"""Returns the display rotation in degrees."""
return self.rotation * 90
def set_all(self, r, g, b):
self.buffer[:] = r, g, b
def set_pixel(self, x, y, r, g, b):
"""Set a single pixel to RGB colour.
:param x: Horizontal position from 0 to width
:param y: Vertical position from 0 to height
:param r: Amount of red from 0 to 255
:param g: Amount of green from 0 to 255
:param b: Amount of blue from 0 to 255
"""
self.buffer[y][x] = r, g, b
def set_matrix(self, matrix):
"""Sets a height x width matrix directly"""
self.reset_clock()
self.buffer = matrix
self.show()
def set_pixel_hsv(self, x, y, h, s=1.0, v=1.0):
"""set a single pixel to a colour using HSV.
:param x: Horizontal position from 0 to 15
:param y: Veritcal position from 0 to 15
:param h: Hue from 0.0 to 1.0 ( IE: degrees around hue wheel/360.0 )
:param s: Saturation from 0.0 to 1.0
:param v: Value (also known as brightness) from 0.0 to 1.0
"""
r, g, b = [int(n*255) for n in colorsys.hsv_to_rgb(h, s, v)]
self.set_pixel(x, y, r, g, b)
def get_pixel(self, x, y):
return tuple(self.buffer[x][y])
def shade_pixels(self, shader):
for x in range(self.WIDTH):
for y in range(self.HEIGHT):
r, g, b = shader(x, y)
self.set_pixel(x, y, r, g, b)
def get_pixels(self):
return self.buffer
def get_shape(self):
"""Return the shape (width, height) of the display."""
return self.WIDTH, self.HEIGHT
def clear(self):
"""Clear the buffer."""
self.buffer.fill(0)
def off(self):
"""Clear the buffer and immediately update Unicorn HAT HD.
Turns off all pixels.
"""
self.clear()
self.show()
def show(self):
"""Output the contents of the buffer to Unicorn HAT HD."""
##########################################################
## Change to desire
buff2 = numpy.rot90(self.buffer[:self.HEIGHT,:16],3)
buff1 = numpy.rot90(self.buffer[:self.HEIGHT,16:32],1)
##########################################################
buff1, buff2 = [(x.reshape(768) * self.brightness).astype(numpy.uint8).tolist() for x in (buff1, buff2)]
self.spi_write(buff1, buff2)
time.sleep(self.DELAY)

View File

@ -1,122 +1,91 @@
import time
import numpy as np
from clock.api import converter
try:
from clock.api.hat import unicorn as HAT
from . import unicorn
output = unicorn.ClockOut
except ImportError:
print("Using the simulator")
from clock.api.hat import sim as HAT
from . import sim
output = sim.ClockOut
# import sim
# output = sim.ClockOut
class OutputHandler():
def __init__(self, width, height, primary = [200, 200, 200], secondary = [10, 200, 10], error = [200, 10, 10]):
"""width is presumed to be larger than height"""
self.width = width
self.height = height
self.output = HAT.UnicornHat(width, height)
self.primary = primary
self.secondary = secondary
self.red = error
"""Matrix of led-points (RGB- values). It has the two given dimensions + a third which is given by the color values"""
# def __new__(subtype, shape, dtype=float, buffer=None, offset=0,
# strides=None, order=None, info=None):
# # Create the ndarray instance of our type, given the usual
# # ndarray input arguments. This will call the standard
# # ndarray constructor, but return an object of our type.
# # It also triggers a call to InfoArray.__array_finalize__
# # expand the given tuple (flat display) to a 3d array containing the colors as well
# nshape = (*shape, 3)
# obj = super(OutputHandler, subtype).__new__(subtype, nshape, "int",
# buffer, offset, strides,
# order)
# # set the new 'info' attribute to the value passed
# obj.info = info
# obj.OUT = output(shape)
# # Finally, we must return the newly created object:
# return obj
def __init__(self, shape):
nshape = (*shape, 3)
self.array = np.array(shape, dtype=np.uint8)
self.OUT = output(shape)
# def __array_finalize__(self, obj):
# self.OUT = sim.ClockOut()
# # ``self`` is a new object resulting from
# # ndarray.__new__(), therefore it only has
# # attributes that the ndarray.__new__ constructor gave it -
# # i.e. those of a standard ndarray.
# #
# # We could have got to the ndarray.__new__ call in 3 ways:
# # From an explicit constructor - e.g. InfoArray():
# # obj is None
# # (we're in the middle of the InfoArray.__new__
# # constructor, and self.info will be set when we return to
# # InfoArray.__new__)
# if obj is None: return
# # From view casting - e.g arr.view(InfoArray):
# # obj is arr
# # (type(obj) can be InfoArray)
# # From new-from-template - e.g infoarr[:3]
# # type(obj) is InfoArray
# #
# # Note that it is here, rather than in the __new__ method,
# # that we set the default value for 'info', because this
# # method sees all creation of default objects - with the
# # InfoArray.__new__ constructor, but also with
# # arr.view(InfoArray).
# self.info = getattr(obj, 'info', None)
# # We do not need to return anything
def set_matrix(self, matrix, quadrant = 1, colors = []):
"""assumes 1 for primary, 2 for secondary color (everything beyond is treated as an error)
quadrant: 1,2,3,4 : 4|1
___
3|2
"""
def SHOW(self):
# self.output.set_matrix(self)
# reshape to the main size: (eg 32x16) (always aligns the given matrix on top left.)
if len(matrix.shape) != 3:
# add depth (rgb values)
matrix = self.matrix_add_depth(matrix,colors)
self.set_matrix_rgb(matrix,quadrant)
self.OUT.put(self.array)
def matrix_add_depth(self, matrix, colors = []):
"""transforms a 2d-array with 0,1,2 to a 3d-array with the rgb values for primary and secondary color"""
c1 = self.primary
c2 = self.secondary
c3 = self.red
if len(colors) > 0:
c1 = colors[0]
if len(colors) > 1:
c2 = colors[1]
if len(colors) > 2:
c3 = colors[2]
if len(colors) > 3:
print("Too many colors")
# def __init__(self, width, height, primary = [200, 200, 200], secondary = [10, 200, 10], error = [200, 10, 10]):
# """width is presumed to be larger than height"""
# self.width = width
# self.height = height
# self.output = HAT.UnicornHat(width, height)
# self.primary = primary
# self.secondary = secondary
# self.red = error
r3 = np.zeros((matrix.shape[0],matrix.shape[1],3),dtype=int)
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
t = int(matrix[i, j])
if t == 0:
r3[i, j, :] = [0,0,0]
elif t == 1:
r3[i, j, :] = c1
elif t == 2:
r3[i, j, :] = c2
else:
r3[i, j, :] = c3
return r3
def set_matrix_rgb(self, matrix, quadrant=1):
result = np.zeros((self.height, self.width,3))
if quadrant == 1:
result[:matrix.shape[0], self.width-matrix.shape[1]:,...] = matrix
elif quadrant == 2:
result[self.height-matrix.shape[0]:, self.width-matrix.shape[1]:,...] = matrix
elif quadrant == 3:
result[self.height-matrix.shape[0]:, :matrix.shape[1],...] = matrix
else: # 4 or more
result[:matrix.shape[0], :matrix.shape[1],...] = matrix
self.output.set_matrix(result)
def clock_face(self, weather):
"""weather as a dict"""
hour = converter.time_converter()
day = converter.date_converter()
face1 = hour + day
face1_3d = self.matrix_add_depth(face1)
if weather["show"] == "weather":
face2_3d = converter.weather_converter(weather["weather"])
else:
face2 = converter.time_converter(top=str(weather["low"]), bottom=str(weather["high"]))
face2 = np.concatenate((face2[:8,...],2*face2[8:,...]))
face2_3d = self.matrix_add_depth(face2,[[0, 102, 255],[255, 102, 0]])
face = np.zeros((max(face1_3d.shape[0],face2_3d.shape[0]),face1_3d.shape[1]+face2_3d.shape[1],3))
face[:face1_3d.shape[0],:face1_3d.shape[1],...] = face1_3d
face[:face2_3d.shape[0],face1_3d.shape[1]:,...] = face2_3d
self.set_matrix_rgb(face)
def text_scroll(self, text, speed, color):
pixels = converter.text_converter(text, 12)
sleep_time = 1 / speed
if color == "":
colors = []
else:
colors = [color]
frames = pixels.shape[1] - self.width
if frames <= 0:
frames = 1
for i in range(frames):
visible = pixels[:,i:self.width+i]
self.set_matrix(visible,4,colors)
time.sleep(sleep_time)
time.sleep(10 * sleep_time)

27
clock/api/sim.py Normal file
View File

@ -0,0 +1,27 @@
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
mpl.rcParams['toolbar'] = 'None'
mpl.use('WX')
class ClockOut():
"""Simulate a clock output on a computer screen"""
def __init__(self, shape):
plt.axis('off')
plt.ion()
nshape = (*shape, 3)
zero = np.zeros(nshape)
self.figure, ax = plt.subplots()
ax.set_axis_off()
i = Image.fromarray(zero, "RGB")
self.canvas = ax.imshow(i)
def put(self, matrix):
matrix_rescale = matrix / 255
self.canvas.set_array(matrix_rescale)
self.figure.canvas.draw()
self.figure.canvas.flush_events()

89
clock/api/unicorn.py Normal file
View File

@ -0,0 +1,89 @@
import colorsys
import time
import numpy
import RPi.GPIO as GPIO
class ClockOut(object):
def __init__(self, shape):
self.PIN_CLK = 11
##################################
# GPIO Pins for the actual signal. The other ones are for signal clocks and resets.
self.PINS_DAT = [10, 22]
##################################
self.PIN_CS = 8
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(self.PIN_CS, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(self.PIN_CLK, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.PINS_DAT, GPIO.OUT, initial=GPIO.LOW)
self.SOF = 0x72
self.DELAY = 1.0/120
self.HEIGHT = shape[0] #16
self.WIDTH = shape[1] #32
self.reset_clock()
def reset_clock(self):
GPIO.output(self.PIN_CS, GPIO.LOW)
time.sleep(0.00001)
GPIO.output(self.PIN_CS, GPIO.HIGH)
def spi_write(self, buf1, buf2):
GPIO.output(self.PIN_CS, GPIO.LOW)
self.spi_write_byte(self.SOF, self.SOF)
for x in range(len(buf1)):
b1, b2= buf1[x], buf2[x]
self.spi_write_byte(b1, b2)
GPIO.output(self.PIN_CS, GPIO.HIGH)
def spi_write_byte(self, b1, b2):
for x in range(8):
GPIO.output(self.PINS_DAT[0], b1 & 0b10000000)
GPIO.output(self.PINS_DAT[1], b2 & 0b10000000)
GPIO.output(self.PIN_CLK, GPIO.HIGH)
b1 <<= 1
b2 <<= 1
#time.sleep(0.00000001)
GPIO.output(self.PIN_CLK, GPIO.LOW)
def put(self, matrix):
"""Sets a height x width matrix directly"""
self.reset_clock()
self.show(matrix)
def clear(self):
"""Clear the buffer."""
zero = np.zero((self.HEIGHT, self. WIDTH))
self.put(zero)
def show(self, matrix):
"""Output the contents of the buffer to Unicorn HAT HD."""
##########################################################
## Change to desire
buff2 = numpy.rot90(matrix[:self.HEIGHT,:16],3)
buff1 = numpy.rot90(matrix[:self.HEIGHT,16:32],1)
##########################################################
buff1, buff2 = [(x.reshape(768)).astype(numpy.uint8).tolist() for x in (buff1, buff2)]
self.spi_write(buff1, buff2)
time.sleep(self.DELAY)

View File

@ -0,0 +1 @@
from . import helper

202
clock/helpers/helper.py Normal file
View File

@ -0,0 +1,202 @@
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import datetime
import time
####### bulky hard-coded values:
digits = {
"1" : [[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]],
"2" : [[1,1,1],[0,0,1],[1,1,1],[1,0,0],[1,1,1]],
"3" : [[1,1,1],[0,0,1],[1,1,1],[0,0,1],[1,1,1]],
"4" : [[1,0,1],[1,0,1],[1,1,1],[0,0,1],[0,0,1]],
"5" : [[1,1,1],[1,0,0],[1,1,1],[0,0,1],[1,1,1]],
"6" : [[1,1,1],[1,0,0],[1,1,1],[1,0,1],[1,1,1]],
"7" : [[1,1,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]],
"8" : [[1,1,1],[1,0,1],[1,1,1],[1,0,1],[1,1,1]],
"9" : [[1,1,1],[1,0,1],[1,1,1],[0,0,1],[1,1,1]],
"0" : [[1,1,1],[1,0,1],[1,0,1],[1,0,1],[1,1,1]],
"-" : [[0,0,0],[0,0,0],[1,1,1],[0,0,0],[0,0,0]],
"-1" : [[0,0,1],[0,0,1],[1,1,1],[0,0,1],[0,0,1]],
"error" : [[1,0,1],[1,0,1],[0,1,0],[1,0,1],[1,0,1]],
} # these are 2-d arrays as we only work with one or 2 colors, and not the whole rgb spectrum
##place of numbers, invariant (for the given shape of 16x32)
digit_position = [[2,4], [2,10], [9,4], [9,10]]
weather_categories = {
"Clouds": "cloud",
"Rain": "rain and cloud",
"Thunderstorm": "thunder and cloud",
"Drizzle": "rain and cloud",
"Snow": "snow and cloud",
"Clear": "sun",
"Mist": "fog and clouds",
"Smoke": "Smoke",
"Haze": "Haze",
"Dust": "Dust",
"Fog": "fog",
"Sand": "Sand",
"Dust": "Dust",
"Ash": "Ash",
"Squal": "Squal",
"Tornado": "Tornado",
"error" : "moon"
}
days = np.append(np.zeros((15,16)), np.array([0,1,0,1,0,1,0,1,0,1,0,1,1,0,1,1])).reshape((16,16))
class MatrixOperations():
"""Helper functions to generate frequently-used images"""
def __init__(self, shape, default_colors):
self.shape = shape
# shape is going to be (16,32) for the moment
self.primary = default_colors["primary"]
self.secondary = default_colors["secondary"]
self.error = default_colors["error"]
def time_converter(self, top="", bottom=""):
"""Converts 4-digit time to a pixel-matrix
returns: np.array wich fills one half of self.shape (horizontally)"""
nshape = (self.shape[0], int(self.shape[1]/2))
pixels = np.zeros(nshape,dtype=np.uint8)
if bottom == "" or top == "":
top = datetime.datetime.now().strftime("%H")
bottom = datetime.datetime.now().strftime("%M")
if len(top) < 2:
top = "0" * (2 - len(top)) + top
if len(bottom) < 2:
bottom = "0" * (2 - len(bottom)) + bottom
if ("-" in top and len(top) > 2) or ("-" in bottom and len(bottom) > 2):
time_split = 4*["-"]
elif "error" in top and "error" in bottom:
time_split = 4*["error"]
else:
time_split = [i for i in top] + [i for i in bottom]
if "-1" in top and len(top) != 2:
time_split = ["-1", top[-1]] + [i for i in bottom]
if "-1" in bottom and len(bottom) != 2:
time_split = [i for i in top] + ["-1", bottom[-1]]
for i in range(4):
x = digit_position[i][0]
y = digit_position[i][1]
number = digits[time_split[i]]
pixels[x: x + 5, y: y + 3] = np.array(number)
return pixels
def date_converter(self):
nshape = (self.shape[0], int(self.shape[1]/2))
today = datetime.datetime.today()
weekday = datetime.datetime.weekday(today)
# size of the reduced array according to weekday
size = [2,4,6,8,10,13,16]
pixels = days.copy() #base color background
lrow = np.append(pixels[15,:size[weekday]], [0 for i in range(16 - size[weekday])])
lrow = np.append(np.zeros((15,16)), lrow).reshape(nshape)
pixels += lrow
return pixels
def weather_converter(self, name):
"""Fills one half of the screen with weather info."""
nshape = (self.shape[0], int(self.shape[1]/2))
result = np.zeros(nshape)
cwd = __file__.replace("\\","/") # for windows
cwd = cwd.rsplit("/", 1)[0] # the current working directory (where this file is)
if len(cwd) == 0:
cwd = "."
icon_spritesheet = cwd + "/weather-icons.bmp"
icons = Image.open(icon_spritesheet)
icons_full = np.array(icons)
icon_loc = ["sun","moon","sun and clouds", "moon and clouds", "cloud","fog and clouds","2 clouds", "3 clouds", "rain and cloud", "rain and clouds", "rain and cloud and sun", "rain and cloud and moon", "thunder and cloud", "thunder and cloud and moon", "snow and cloud", "snow and cloud and moon", "fog","fog night"]
#ordered 1 2 \n 3 4 \ 5 5 ...
name = weather_categories[name]
try:
iy, ix = int(icon_loc.index(name)/2), icon_loc.index(name)%2
# x and y coords
except:
return np.zeros((*nshape,3))
icon_single = icons_full[16*iy:16*(iy + 1),16*ix:16*(ix + 1),...]
return icon_single
def matrix_add_depth(self, matrix, colors = []):
"""transforms a 2d-array with 0,1,2 to a 3d-array with the rgb values for primary and secondary color"""
c1 = self.primary
c2 = self.secondary
c3 = self.error
if len(colors) > 0:
c1 = colors[0]
if len(colors) > 1:
c2 = colors[1]
if len(colors) > 2:
c3 = colors[2]
if len(colors) > 3:
print("Too many colors")
r3 = np.zeros((matrix.shape[0],matrix.shape[1],3),dtype=int)
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
t = int(matrix[i, j])
if t == 0:
r3[i, j, :] = [0,0,0]
elif t == 1:
r3[i, j, :] = c1
elif t == 2:
r3[i, j, :] = c2
else:
r3[i, j, :] = c3
return r3
def clock_face(self, weather):
"""weather as a dict"""
hour = self.time_converter()
day = self.date_converter()
face1 = hour + day
face1_3d = self.matrix_add_depth(face1)
if weather["show"] == "weather":
face2_3d = self.weather_converter(weather["weather"])
else:
face2 = self.time_converter(top=str(weather["low"]), bottom=str(weather["high"]))
face2 = np.concatenate((face2[:8,...],2*face2[8:,...]))
face2_3d = self.matrix_add_depth(face2,[[0, 102, 255],[255, 102, 0]])
face = np.zeros((max(face1_3d.shape[0],face2_3d.shape[0]),face1_3d.shape[1]+face2_3d.shape[1],3))
face[:face1_3d.shape[0],:face1_3d.shape[1],...] = face1_3d
face[:face2_3d.shape[0],face1_3d.shape[1]:,...] = face2_3d
return face
def text_converter(self, text, height, color):
"""Converts a text to a pixel-matrix
returns: np.array((16, x, 3))"""
font = ImageFont.truetype("verdanab.ttf", height)
size = font.getsize(text)
img = Image.new("1",size,"black")
draw = ImageDraw.Draw(img)
draw.text((0, 0), text, "white", font=font)
pixels = np.array(img, dtype=np.uint8)
pixels3d = self.matrix_add_depth(pixels, color)
return pixels3d

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -4,19 +4,26 @@ import json
from threading import Thread, Timer
import numpy
from clock.api import led
from . import api, helpers
class ClockFace(object):
"""Actual functions one might need for a clock"""
def __init__(self, text_speed=18, prst=""):
def __init__(self, text_speed=18, prst=object):
""""""
# added by the launcher, we have self.modules (dict)
self.persistence = prst
self.IO = led.OutputHandler(32,16)
# hard coded, but can be changed to taste
self.tspeed = text_speed
self.primary = [200, 200, 200]
self.secondary = [10, 200, 10]
self.error = [200, 10, 10]
self.shape = (16,32)
# shape: (16,32) is hard-coded for the moment
self.persistence = prst
self.IO = api.led.OutputHandler(self.shape)
self.MOP = helpers.helper.MatrixOperations(self.shape, default_colors={"primary": self.primary, "secondary": self.secondary, "error": self.error})
self.output_thread = ""
# Action the thread is currently performing
@ -25,11 +32,13 @@ class ClockFace(object):
self.weather = {"weather":"", "high":"", "low":"", "show":"temps"}
self.weather_raw = {}
# different?
self.brightness = 1
self.brightness_overwrite = {"value" : 1, "duration" : 0}
def start(self):
self.clock_loop()
while datetime.datetime.now().strftime("%H%M%S")[-2:] != "00":
pass
RepeatedTimer(60, self.clock_loop)
@ -61,7 +70,7 @@ class ClockFace(object):
next = "weather"
self.weather["show"] = next
self.set_face()
self.run(self.set_face,())
def run(self, command, kw=()):
@ -76,7 +85,7 @@ class ClockFace(object):
n = self.output_queue.pop(0)
enhanced_run(n[0],n[1])
else:
self.IO.clock_face(self.weather)
self.set_face()
if len(self.output_thread) == 0:
t = Thread(target=enhanced_run, args=(command, kw))
@ -88,18 +97,19 @@ class ClockFace(object):
############################################################################
### basic clock commands
def set_face(self):
""""""
self.run(self.IO.clock_face,(self.weather,))
"""Set the clock face (time + weather) by getting updated info - gets called every minute"""
face = self.MOP.clock_face(self.weather)
self.IO.array = face * self.brightness
self.IO.SHOW()
def set_brightness(self, overwrite=[],value=-1):
def set_brightness(self, value=-1, overwrite=[]):
"""Checks, what brightness rules to apply"""
if value != -1:
self.IO.output.set_brightness(value)
self.brightness = value
return
if len(overwrite) != 0:
self.brightness_overwrite = overwrite
@ -110,8 +120,22 @@ class ClockFace(object):
else:
brightness = 0.01
self.IO.output.set_brightness(brightness)
self.brightness = brightness
def text_scroll(self, text, color=[[200,200,200]]):
pixels = self.MOP.text_converter(text, 12, color)
sleep_time = 1 / self.tspeed
width = self.shape[1]
frames = pixels.shape[1] - width
if frames <= 0:
frames = 1
for i in range(frames):
visible = pixels[:,i:width+i]
self.IO.array = visible*self.brightness
self.IO.SHOW()
time.sleep(sleep_time)
time.sleep(10 * sleep_time)

BIN
image.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB