From 666a377e32ccd5ceafe0277dcc51affc5b1ac027 Mon Sep 17 00:00:00 2001 From: KS_HTK Date: Wed, 20 Aug 2025 23:15:15 +0200 Subject: [PATCH] initial commit - added readme - added recharge scripts - added dots simple test script - added images, transitions and animations script --- .env.example | 1 + .gitignore | 1 + README.md | 18 +++++ dots.py | 17 +++++ images.py | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++ recharge.py | 87 ++++++++++++++++++++++ recharge2.py | 81 +++++++++++++++++++++ 7 files changed, 404 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 dots.py create mode 100644 images.py create mode 100644 recharge.py create mode 100644 recharge2.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dcd31f8 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DOTS_HOST=fluepdot.local \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f166b8b --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Fluepdot Scripts + +Collection of runnable scripts to display stuff on the fluepdot module in the flipdot hackspace + +All scripts should load the hostname(s) for the modules from the `.env` file. + +Requirements for each script may vary but most will use the python-fluepdot library. +All requirements should be listed in the main script file. + + + +## Notes: + +### Flashing firmware + +``` +esptool.py -p /dev/ttyUSB0 -b 115200 --before default_reset --after hard_reset --chip esp32 write_flash --flash_mode keep --flash_size detect --flash_freq 40m 0x800 partition_table/partition-table.bin 0xd000 bootloader/bootloader.bin 0x10000 ./flipdot-firmware.bin 0xD000 ota_data_initial.bin +``` \ No newline at end of file diff --git a/dots.py b/dots.py new file mode 100644 index 0000000..62382b5 --- /dev/null +++ b/dots.py @@ -0,0 +1,17 @@ +""" +Small sample Script to get size, mode and fonts from the Module and display it's Hostname +""" +# requires: python-fluepdot, python-dotenv +import os +from dotenv import load_dotenv +from fluepdot import Fluepdot, Mode + +load_dotenv() +hostname = os.getenv("DOTS_HOST") + + +fd = Fluepdot(f"http://{hostname}") +#fd.post_text("Current Time:", x=0, y=-1, font="DejaVuSerif16") +#fd.clear() +print(fd.get_size(), fd.get_mode(), fd.get_fonts()) +fd.post_text(f"{hostname}", font="DejaVuSans12bw_bwfont") \ No newline at end of file diff --git a/images.py b/images.py new file mode 100644 index 0000000..e71ec45 --- /dev/null +++ b/images.py @@ -0,0 +1,199 @@ +""" +This file contains animations, stillframes and transitions that can be imported elsewhere. +When running this file it will randomly select a transition and a frame to transition to indefinitly. +""" +# requires: python-fluepdot, python-dotenv +import os +from dotenv import load_dotenv +from src.fluepdot.fluepdot import Fluepdot, Mode +from random import choice, sample +from itertools import product +from time import sleep +from typing import List + +DELAY = 5 # hold time for completed animations/frames +ANIMATION_DELAY = 0.1 # delay between frames of animations + +flipdot_logo = [ + " XXXXXX ", + " XX X ", + " X X ", + " X X ", + " X XX ", + "X X", + "X X X X", + "X X X X X", + "X X XXX X", + "X XXX X X X", + "X X X X X", + " X X XXX X ", + " X X ", + " X X ", + " XX XX ", + " XXXXXX ", +] + + +fd1 = Fluepdot(f"http://{ip1}") +#fd2 = Fluepdot(f"http://{ip2}", flipped=True) + +def checkerboard(len_x: int, len_y: int, invert: bool = False) -> List[List[bool]]: + return [[(j+i+invert)%2==0 for i in range(len_x)] for j in range(len_y)] + +def droplet(fd: Fluepdot): + max_x, max_y = fd.get_size() + fd.set_mode(Mode.DIFFERENTIAL) + frame = [[False for _ in range(max_x)] for _ in range(max_y)] + + cx, cy = max_x//2-.5, max_y//2-.5 + + r = 0 + while r < cx+32: + for y in range(max_y): + for x in range(int(cx-r-2), int(cx+r+3)): + if not (0 <= x < max_x and 0 <= y < max_y): + continue + frame[y][x] = False + dist = ((x-cx)**2+(y-cy)**2)**.5 + for l in range(5): + if r-1-(l**1.4)*4 < dist < r+1-(l**1.4)*4: + frame[y][x] = True + break + r+=2 + if r > 36: + fd_logo_width = len(flipdot_logo[0]) + s = int(cx+1 - fd_logo_width//2) + e = s + fd_logo_width + for y in range(max_y): + for x in range(s, e): + frame[y][x] = flipdot_logo[y][x-s] == "X" or frame[y][x] + + sleep(ANIMATION_DELAY) + fd.post_frame(frame) + +def fd_logo(fd: Fluepdot) -> List[List[bool]]: + fd.post_text("flipdot e.V.", x=7, y=0, font="fixed_7x14") + original_frame = fd.get_frame() + frame = [] + for lrow, frow in zip(flipdot_logo, original_frame): + new_row = frow[:-(len(lrow)+2)]+lrow+frow[-2:] + frame.append(new_row) + return [[c=="X" for c in row] for row in frame] + +def kurhacken(fd: Fluepdot) -> List[List[bool]]: + fd.post_text("Kurhecken", x=14, y=-2, font="fixed_10x20") + frame = [[c=="X" for c in row] for row in fd.get_frame()] + fd.set_mode(Mode.DIFFERENTIAL) + for x, y in [(56, 4),(57, 4),(60, 4),(61, 4),(56, 3),(57, 3),(60, 3),(61, 3)]: + frame[y][x] = True + return frame + +def event37c3(fd: Fluepdot) -> List[List[bool]]: + fd.post_text("37c3Unlocked", x=-1, y=-1, font="DejaVuSerif16") + frame = [[c=="X" for c in row] for row in fd.get_frame()] + return frame + +def event38c3(fd: Fluepdot) -> List[List[bool]]: + fd.post_text("38C3 Illegal Instructions", font="DejaVuSerif16") + frame = [[c=="X" for c in row] for row in fd.get_frame()] + return frame + +def hackumenta(fd: Fluepdot) -> List[List[bool]]: + fd.post_text("Hackumenta", x=3, y=-1, font="DejaVuSerif16") + frame = [[c=="X" for c in row] for row in fd.get_frame()] + return frame + +def wipe_to(fd: Fluepdot, frame: List[List[bool]]): + fd.set_mode(Mode.DIFFERENTIAL) + current = [[c=="X" for c in row] for row in fd.get_frame()] + for i in range(0, len(current[0])): + for j in range(len(current)): + current[j][i] = frame[j][i] + if i+1 < len(current[0]): + current[j][i+1] = True + sleep(ANIMATION_DELAY) + fd.post_frame(current) + +def dither_to(fd: Fluepdot, frame: List[List[bool]]): + fd.set_mode(Mode.DIFFERENTIAL) + max_x, max_y = fd.get_size() + current = [[c=="X" for c in row] for row in fd.get_frame()] + coords = [(x,y) for x, y in product(range(max_x), range(max_y)) if current[y][x] != frame[y][x]] + while coords: + sleep(ANIMATION_DELAY) + x, y = choice(coords) + coords.remove((x, y)) + if frame[y][x]: + r = fd.set_pixel(x, y) + if r.status_code != 200: + print(r.text) + else: + r = fd.unset_pixel(x, y) + if r.status_code != 200: + print(r.text) + +def push_to(fd: Fluepdot, frame: List[List[bool]]): + fd.set_mode(Mode.DIFFERENTIAL) + current = [[c=="X" for c in row] for row in fd.get_frame()] + while True: + sleep(max(ANIMATION_DELAY, 1)) + for crow, frow in zip(current, frame): + for _ in range(3): + crow.insert(0, frow.pop(-1)) + crow.pop(-1) + if frow: + continue + else: + break + else: + fd.post_frame(current) + continue + break + +def roll_to(fd: Fluepdot, frame: List[List[bool]]): + fd.set_mode(Mode.DIFFERENTIAL) + current = [[c=="X" for c in row] for row in fd.get_frame()] + while frame: + sleep(max(ANIMATION_DELAY, 1.5)) + current.insert(0, frame.pop()) + current.pop() + fd.post_frame(current) + +if __name__ == "__main__": + load_dotenv() + hostname = os.getenv("DOTS_HOST") + fd = Fluepdot(f"http://{hostname}") + fd1.clear() + animations = [ + droplet, + ] + transitions = [ + wipe_to, + dither_to, + push_to, + roll_to, + ] + stillframes = [ + kurhacken(fd), # Text endpoint + checkerboard(*fd.get_size(), invert=False), # Frame endpoint + [[True]*115]*16, + [[False]*115]*16, + #event37c3(fd), # Text endpoint + event38c3(fd), # Text endpoint + hackumenta(fd), + fd_logo(fd), # Text endpoint + ] + #for i, f in enumerate(stillframes): + # print(i, len(f), [len(g) for g in f]) + + current = stillframes[1] + while True: + sf = sample(stillframes, k=1)[0] + tr = choice(transitions) + if sf == current: + print("Skipping same frame") + continue + print(tr.__name__) + current = sf + tr(fd, sf) + sleep(DELAY) diff --git a/recharge.py b/recharge.py new file mode 100644 index 0000000..f0dd6d9 --- /dev/null +++ b/recharge.py @@ -0,0 +1,87 @@ +""" +Script for the flipdot recharge party animation +""" +# requires: python-fluepdot, python_dotenv +import os +from time import sleep +from dotenv import load_dotenv + +from fluepdot import Fluepdot, Mode + +logo = """ + + X + XXXXXXXXXXXX XX XXXXXXXXXXX + XX XXX XX + X XXX X + X XXXX X + X XXXXXXXX XX + X XXXXXXXX XX + X XXXX X + X XXX X + XX XXX XX + XXXXXXXXXXX XX XXXXXXXXXXXX + X + + +""" + +# convert text to framebuffer format +def textToFramebuffer(text: str, x:int=0, y:int=0, fontname:str="DejaVuSans.ttf", size:int=14) -> str: + from PIL import Image, ImageFont, ImageDraw + font = ImageFont.truetype(fontname, size) + tw, th = font.getsize(text) + while th >= 16: + print(th) + size -= 1 + font = ImageFont.truetype(fontname, size) + tw, th = font.getsize(text) + img = Image.new("1", (tw, 16), 0) + draw = ImageDraw.Draw(img) + draw.text((0, 0), text=text, font=font, fill=1) + + framebuffer: str = "" + for y in range(img.height): + for x in range(img.width): + framebuffer += "X" if img.getpixel((x, y)) == 1 else " " + framebuffer += "\n" + return framebuffer + + +def scrl_frm(frame, loop: bool or int = False) -> None: + def _extend_frame(line: str) -> str: + return line+(" "*loop)+line[0:115] + def _pad_frame(line: str) -> str: + pad = " "*115 + return pad+line+pad + + frame = frame.split("\n") + length = len(frame[0])+loop if type(loop) == int else len(frame[0])+115 + + if type(loop) == int: + frame = list(map(_extend_frame, frame)) + else: + frame = list(map(_pad_frame, frame)) + + _run_once=True + while loop or type(loop)==int or _run_once: + _run_once=False + for i in range(0, length, 2): + print(".\n".join(l[i:i+115] for l in frame)) + sleep(.1) + +if __name__=="__main__": + load_dotenv() + hostname = os.getenv("DOTS_HOST") + frame = textToFramebuffer("recharge! flipdot e.V.", fontname="DejaVuSans.ttf") + fd = Fluepdot(f"http://{hostname}") + fd.set_mode(Mode.DIFFERENTIAL) + + logoarr = logo.split("\n") + nframe = "" + for y in range(len(logoarr)): + nframe += logoarr[y] + frame.split("\n")[y]+ "\n" + nframe = nframe[:-1] + print(nframe) + fd.post_scroll_frame_raw(nframe, loop=2) + #scrl_frm(nframe, loop=2) diff --git a/recharge2.py b/recharge2.py new file mode 100644 index 0000000..215a3bb --- /dev/null +++ b/recharge2.py @@ -0,0 +1,81 @@ +""" +Alternative animation for the flipdot recharge party +""" +# requires: python-fluepdot, python-dotenv +import os +from dotenv import load_dotenv +from fluepdot import Fluepdot, Mode +from time import sleep + +battery = [ +" XXXXXXXXXXXX XXXXXXXXXXX ", +"XX XX ", +"X X ", +"X X ", +"X XX", +"X XX", +"X X ", +"X X ", +"XX XX ", +" XXXXXXXXXXX XXXXXXXXXXXX "] + +lightning=[" X ", + " XX ", + " XXX ", + " XXX ", + " XXXX ", + "XXXXXXXX "] + +def get_lightning(level: int) -> str: + if level < 0: + level = 0 + elif level > 12: + level = 12 + + rtn = "\n".rjust(19)+"\n".rjust(19) + + for i in range(0, level): + line = lightning[i] if i < len(lightning) else lightning[len(lightning)-i-1][::-1] + rtn = line.rjust(19)+"\n"+rtn + while rtn.count("\n") < 16: + rtn="\n".rjust(19)+rtn + return rtn + +def get_battery(level: int) -> str: + lightning = get_lightning(level).split("\n")[:-1] + + rtn = "" + for ln in range(0, len(lightning)): + if ln < 3: + rtn+=lightning[ln].ljust(30) + elif ln > 2+len(battery): + rtn+=lightning[ln].ljust(30) + else: + for cind in range(0, len(battery[0])): + try: + rtn += "X" if battery[ln-3][cind] == "X" or lightning[ln][cind] == "X" else " " + except IndexError: + rtn += " " + rtn += "\n" + + return rtn + +if __name__=="__main__": + load_dotenv() + hostname = os.getenv("DOTS_HOST") + fd = Fluepdot(f"http://{hostname}") + fd.set_mode(Mode.DIFFERENTIAL) + l, h = fd.get_size() + while True: + for i in range(0, 12): + bat = get_battery(i).split("\n")[:-1] + bat = list(map(lambda x: x.rjust(l), bat)) + bat = "\n".join(bat)+"\n" + fd.post_frame_raw(frame=bat) + sleep(.5) + for i in range(12, 0, -1): + bat = get_battery(i).split("\n")[:-1] + bat = list(map(lambda x: x.rjust(l), bat)) + bat = "\n".join(bat)+"\n" + fd.post_frame_raw(frame=bat) + sleep(.5)