# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import array import random import time import asynced import iptime from neopixel_write import neopixel_write import board import machine import neopixel RANGES = { 'dude': 0, 'ok': 5, 'ten': 8, 'seven': 11, 'twelve': 16, 'even': 22, 'eleven': 22, 'eight': 28, 'four': 33, 'five': 37, 'two': 41, 'three': 44, 'six': 49, 'one': 52, 'past': 55, 'run': 60, 'nine': 62, 'to': 66, 'TEN': 69, 'HALF': 73, 'TWENTY': 77, 'TWENTYFIVE': 77, 'FIVE': 83, 'QUARTER': 90, 'A': 98, 'AM': 106, 'PM': 108, 'is': 102, 'it': 99, 'its': 99, } CIEL8 = (0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 8, 8, 9, 10, 11, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 28, 30, 31, 33, 35, 37, 39, 40, 42, 44, 46, 49, 51, 53, 55, 57, 60, 63, 66, 68, 71, 74, 77, 80, 83, 86, 90, 93, 96, 100, 103, 107, 111, 115, 118, 122, 127, 131, 136, 140, 144, 149, 153, 158, 163, 168, 173, 179, 184, 189, 195, 200, 206, 212, 218, 224, 230, 236, 242, 249, 255) MINUTES = ('oclock', 'FIVE past', 'TEN past', 'QUARTER past', 'TWENTY past', 'TWENTYFIVE past', 'HALF past', 'TWENTYFIVE to', 'TWENTY to', 'QUARTER to', 'TEN to', 'FIVE to') HOURS = ('twelve', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven') AMPM = ('AM', 'PM') ITS = ('its', 'it is', 'its dude', 'it is dude', 'its ok', 'it is ok') COLOUR = (200, 255, 200) def split(secs: int) -> tuple: secs = int(secs) mins = (secs // 60) % 60 minute = mins // 5 hour = (secs // 3600) % 24 if mins >= 35: hour += 1 ampm = (hour // 12) % len(AMPM) hour %= len(HOURS) return MINUTES[minute], HOURS[hour], AMPM[ampm] def to_rgb(i, colour) -> tuple: return ((i * colour[0]) >> 8, (i * colour[1]) >> 8, (i * colour[2]) >> 8) class Frame: Max = 255 Zeros = tuple([0] * (11 * 10)) def __init__(self, n: int = 11 * 10, pixels: list = None): if pixels is None: pixels = list(self.Zeros) self.pixels = pixels def set(self, idx: int) -> 'Frame': self.pixels[idx] = self.Max return self def __add__(self, other): return Frame(pixels=[ min(self.Max, x + y) for x, y in zip(self.pixels, other.pixels) ]) def muldiv(self, num: int, den: int): return Frame(pixels=[x * num // den for x in self.pixels]) @micropython.native def merge(f1: Frame, f2: Frame, num1: int, num2: int, den: int) -> Frame: m = Frame.Max p1 = f1.pixels p2 = f2.pixels o = array.array('H', Frame.Zeros) for i in range(len(p1)): k = (p1[i] * num1 + p2[i] * num2) // den if k >= m: k = m o[i] = k return Frame(pixels=o) def render(words: list) -> Frame: f = Frame() for word in words: for w in word.split(): idx = RANGES.get(w, None) if idx is not None: for i in range(len(w)): f.set(idx + i) else: print('warning: %s is missing' % w) return f def prefix() -> tuple: return (ITS[random.randint(0, len(ITS) - 1)], ) def show(f: Frame, n): p = f.pixels buf = bytearray(len(p) * 3) r, g, b = COLOUR i = 0 for c in p: buf[i + 1] = (r * c) >> 8 buf[i + 0] = (g * c) >> 8 buf[i + 2] = (b * c) >> 8 i += 3 neopixel_write(n.pin, buf) def scan() -> generator: f = Frame() while True: for i in range(len(f.pixels)): yield f.set(i) f = f.muldiv(80, 100) def intensity(num: int, den: int) -> int: idx = num * len(CIEL8) // den if idx >= len(CIEL8): return 255 return CIEL8[idx] def brightness(secs): hour = int(secs) // 60 // 60 if hour >= 7 and hour < 22: return 255 return 1200 def run() -> generator: words = None fade = 2000 secs = iptime.day_sec() # Flash until time is synced. sc = scan() for s, meta in secs: if s is not None and meta is not None: break yield next(sc) src = Frame() while True: yield None s, m2 = next(secs) nxt = split(s) if nxt == words: yield from asynced.delay(2) continue dest = render(prefix() + nxt) for at in asynced.wait(fade): yield merge(src, dest, intensity(fade - at, fade), intensity(at, fade), brightness(s)) words = nxt src = dest def blink() -> generator: led = machine.Pin(16, machine.Pin.OUT) while True: led.off() yield from asynced.delay(0.03) led.on() yield from asynced.delay(2.9) def bench() -> generator: yield None i = 30 while True: start = time.monotonic() yield from range(i) elapsed = time.monotonic() - start print('%d in %.3f %.3f/1 %.1f FPS' % (i, elapsed, elapsed / i, i / elapsed)) def main(): num = 11 * 10 n = neopixel.NeoPixel(board.GPIO2, num, brightness=1, auto_write=False) routines = (run(), blink(), asynced.fps(30), bench()) while True: for r in routines: f = next(r) if f is None: continue elif isinstance(f, Frame): show(f, n)