From c61ee3ea728d55f781f26d5311fe6667b57a60cc Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Tue, 23 Feb 2021 15:35:31 +0100 Subject: [PATCH] Better LED-interface (to be tested) --- bot2/commands/clock.py | 32 ++-- clock/api/__init__.py | 1 + clock/api/converter.py | 130 --------------- clock/api/hat/__init__.py | 1 - clock/api/hat/sim.py | 93 ----------- clock/api/hat/tester.py | 13 -- clock/api/hat/unicorn.py | 151 ----------------- clock/api/led.py | 189 +++++++++------------ clock/api/sim.py | 27 +++ clock/api/unicorn.py | 89 ++++++++++ clock/helpers/__init__.py | 1 + clock/helpers/helper.py | 202 +++++++++++++++++++++++ clock/{api => helpers}/weather-icons.bmp | Bin clock/main.py | 52 ++++-- image.jpg | Bin 34329 -> 0 bytes launcher.py | 2 +- 16 files changed, 456 insertions(+), 527 deletions(-) delete mode 100644 clock/api/converter.py delete mode 100644 clock/api/hat/__init__.py delete mode 100644 clock/api/hat/sim.py delete mode 100644 clock/api/hat/tester.py delete mode 100644 clock/api/hat/unicorn.py create mode 100644 clock/api/sim.py create mode 100644 clock/api/unicorn.py create mode 100644 clock/helpers/__init__.py create mode 100644 clock/helpers/helper.py rename clock/{api => helpers}/weather-icons.bmp (100%) delete mode 100644 image.jpg diff --git a/bot2/commands/clock.py b/bot2/commands/clock.py index 7591372..9044b2e 100644 --- a/bot2/commands/clock.py +++ b/bot2/commands/clock.py @@ -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 diff --git a/clock/api/__init__.py b/clock/api/__init__.py index dcf2c80..568a664 100644 --- a/clock/api/__init__.py +++ b/clock/api/__init__.py @@ -1 +1,2 @@ # Placeholder +from . import led \ No newline at end of file diff --git a/clock/api/converter.py b/clock/api/converter.py deleted file mode 100644 index 4ba5381..0000000 --- a/clock/api/converter.py +++ /dev/null @@ -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 diff --git a/clock/api/hat/__init__.py b/clock/api/hat/__init__.py deleted file mode 100644 index dcf2c80..0000000 --- a/clock/api/hat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Placeholder diff --git a/clock/api/hat/sim.py b/clock/api/hat/sim.py deleted file mode 100644 index 5f3430f..0000000 --- a/clock/api/hat/sim.py +++ /dev/null @@ -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() diff --git a/clock/api/hat/tester.py b/clock/api/hat/tester.py deleted file mode 100644 index 98415de..0000000 --- a/clock/api/hat/tester.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/clock/api/hat/unicorn.py b/clock/api/hat/unicorn.py deleted file mode 100644 index cf0dddf..0000000 --- a/clock/api/hat/unicorn.py +++ /dev/null @@ -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) diff --git a/clock/api/led.py b/clock/api/led.py index 124e20a..b57e965 100644 --- a/clock/api/led.py +++ b/clock/api/led.py @@ -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 SHOW(self): + # self.output.set_matrix(self) + + self.OUT.put(self.array) + + + # 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 - 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 - """ + - # 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) - - - 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") - - - 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) diff --git a/clock/api/sim.py b/clock/api/sim.py new file mode 100644 index 0000000..6792050 --- /dev/null +++ b/clock/api/sim.py @@ -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() diff --git a/clock/api/unicorn.py b/clock/api/unicorn.py new file mode 100644 index 0000000..a105b55 --- /dev/null +++ b/clock/api/unicorn.py @@ -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) diff --git a/clock/helpers/__init__.py b/clock/helpers/__init__.py new file mode 100644 index 0000000..34fdc42 --- /dev/null +++ b/clock/helpers/__init__.py @@ -0,0 +1 @@ +from . import helper \ No newline at end of file diff --git a/clock/helpers/helper.py b/clock/helpers/helper.py new file mode 100644 index 0000000..56f6ad7 --- /dev/null +++ b/clock/helpers/helper.py @@ -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 diff --git a/clock/api/weather-icons.bmp b/clock/helpers/weather-icons.bmp similarity index 100% rename from clock/api/weather-icons.bmp rename to clock/helpers/weather-icons.bmp diff --git a/clock/main.py b/clock/main.py index a0c5cea..53e80ac 100644 --- a/clock/main.py +++ b/clock/main.py @@ -4,20 +4,27 @@ 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 self.output_queue = [] @@ -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) diff --git a/image.jpg b/image.jpg deleted file mode 100644 index a3928aa0f935d4ab01d5d588e82a64cf62cfa2a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34329 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 diff --git a/launcher.py b/launcher.py index d7bdd72..b5de8bc 100644 --- a/launcher.py +++ b/launcher.py @@ -73,4 +73,4 @@ class Launcher(): ######################################################################## ## Aand liftoff! -Launcher() \ No newline at end of file +Launcher()