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 a3928aa..0000000 Binary files a/image.jpg and /dev/null differ 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()