diff --git a/.gitignore b/.gitignore index 96dd449..fed370c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ error.glsl forge_default_save.txt build +tmp* diff --git a/2026-10-28_21-38.png b/2026-10-28_21-38.png new file mode 100644 index 0000000..87256a4 Binary files /dev/null and b/2026-10-28_21-38.png differ diff --git a/Makefile b/Makefile index 7aac886..881c2d5 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ run: $(BUILD_DIR)/$(TARGET) .PHONY: start-bg start-bg: - @python3 $(PROJECT_DIR)/i2c.py& + @python3 $(PROJECT_DIR)/i2c.py >> $(PROJECT_DIR)/python.log& .PHONY: start-fg start-fg: diff --git a/Px437_IBM_BIOS.ttf b/Px437_IBM_BIOS.ttf new file mode 100644 index 0000000..c55a10e Binary files /dev/null and b/Px437_IBM_BIOS.ttf differ diff --git a/README.md b/README.md index 189bb60..80da000 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ echo 'make -C /opt/forge-invar start-weston' >> ~/.bashrc * https://www.exploding-shed.com/synth-diy-guides/standards-of-eurorack/eurorack-dimensions/ * https://electricalflux.com/mcu-raspberry/i2c-oled-display-raspberry-pi-5-wiring-code * https://www.raspberrypi.com/documentation/computers/configuration.html +* https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_bios ## TODO diff --git a/TinyAndChunkyRegular.ttf b/TinyAndChunkyRegular.ttf new file mode 100755 index 0000000..f705ea1 Binary files /dev/null and b/TinyAndChunkyRegular.ttf differ diff --git a/host_generate.py b/host_generate.py new file mode 100644 index 0000000..007b0b0 --- /dev/null +++ b/host_generate.py @@ -0,0 +1,106 @@ +from PIL import Image, ImageDraw, ImageFont, ImageMath + +# TODO clean + +font = ImageFont.truetype("./TinyAndChunkyRegular.ttf", 5) +font2 = ImageFont.truetype("./Px437_IBM_BIOS.ttf", 8) + +im0 = Image.new("1", (128, 64), "black") + +draw = ImageDraw.Draw(im0) + +draw.text((26, 24), "F.O.R.G.E.", fill="white", font=font2) +draw.rectangle((24, 35, 104, 40), outline="white", fill="black") + +im0.save("i2c_loading.png") + +im = Image.new("1", (128, 64), "black") + +draw = ImageDraw.Draw(im) + + +# tempo +draw.text((7, -2), "TEMPO:", fill="white", font=font) +draw.rectangle((6, 6, 60, 11), outline="white", fill="black") + +# time +draw.text((68, -2), "TIME:", fill="white", font=font) +draw.rectangle((67, 6, 121, 11), outline="white", fill="black") + +# pipes +draw.rectangle((26, 22, 78, 24), fill="white") +draw.rectangle((26, 46, 78, 48), fill="white") +draw.rectangle((78, 34, 128, 36), fill="white") +draw.rectangle((76, 23, 78, 46), fill="white") + +# squares +draw.rectangle((12, 16, 26, 30), outline="white", fill="black") +draw.rectangle((41, 16, 55, 30), outline="white", fill="black") + +draw.rectangle((12, 40, 26, 54), outline="white", fill="black") +draw.rectangle((41, 40, 55, 54), outline="white", fill="black") + +draw.rectangle((70, 28, 84, 42), outline="white", fill="black") +draw.rectangle((99, 28, 113, 42), outline="white", fill="black") + +selected_bounds = [ + (10, 14, 28, 32), + (10, 38, 28, 56), + (39, 14, 57, 32), + (39, 38, 57, 56), + (0, 0, 0, 0), + (97, 26, 115, 44), +] + +# page +draw.circle((55, 61), 2, outline="white", fill="black") +draw.circle((63, 61), 2, outline="white", fill="black") +draw.circle((71, 61), 2, outline="white", fill="black") + +im.save("i2c_background.png") + +# --- + +# tempo +draw.text((42, -2), "240", fill="white", font=font) +draw.rectangle((6, 7, 6 + 53 * 1, 10), fill="white", width=0) + +# time +draw.text((94, -2), "1234", fill="white", font=font) +draw.rectangle((67, 7, 67 + 53 * 1, 10), fill="white", width=0) + +draw.circle((55, 61), 2, fill="white") + +# selected + +for i in range(len(selected_bounds)): + if selected_bounds[i][0] > 0: + draw.rectangle(selected_bounds[i], outline="white", fill=None) + +im_overlay = Image.new("1", (128, 64), "black") +draw2 = ImageDraw.Draw(im_overlay) + + +# shaders +draw2.text((16, 20), "0", fill="white", font=font2) +draw2.text((45, 20), "2", fill="white", font=font2) +draw2.text((16, 44), "1", fill="white", font=font2) +draw2.text((45, 44), "3", fill="white", font=font2) +draw2.text((74, 32), "K", fill="white", font=font2) +draw2.text((103, 32), "6", fill="white", font=font2) + +im = ImageMath.lambda_eval(lambda args: args["convert"](args["a"] ^ args["b"], "1"), a=im, b=im_overlay) + +# --- + +im = im.resize((1280, 640)).convert("RGB") + +draw2 = ImageDraw.Draw(im) + +for x in range(1, 128): + draw2.line((x * 10, 0, x * 10, 640), fill=(50, 50, 50)) +for y in range(1, 64): + draw2.line((0, y * 10, 1280, y * 10), fill=(50, 50, 50)) + + +im.resize((1280, 640)).save("tmp.png") diff --git a/i2c.py b/i2c.py index 0b7aeae..bbb612d 100755 --- a/i2c.py +++ b/i2c.py @@ -5,38 +5,64 @@ import collections import pathlib import sys +import time from luma.core.error import DeviceNotFoundError from luma.core.interface.serial import i2c -from luma.core.render import canvas -from luma.oled.device import sh1106 +from luma.oled.device import device, sh1106 +from PIL import Image, ImageDraw, ImageFont, ImageMath TARGET_FILE = pathlib.Path(__file__).parent / "forge_default_save.txt" TARGET_FILE_BACKUP = pathlib.Path(__file__).parent / "forge_default_save.txt.bck" +ESTIMATED_BOOT_TIME_SECONDS = 18.0 + data: dict[str, float] = collections.defaultdict(lambda: 0) -def readfile(): +font1 = ImageFont.truetype("./TinyAndChunkyRegular.ttf", 5) +font2 = ImageFont.truetype("./Px437_IBM_BIOS.ttf", 8) +loading_img = Image.open("i2c_loading.png").convert("1") +background_img = Image.open("i2c_background.png").convert("1") +blank_im = Image.new("1", (128, 64), "black") + +selected_bounds = [ + (10, 14, 28, 32), + (10, 38, 28, 56), + (39, 14, 57, 32), + (39, 38, 57, 56), + (0, 0, 0, 0), + (97, 26, 115, 44), +] + + +def try_restore_backup() -> bool: + if TARGET_FILE_BACKUP.exists() and readfile(TARGET_FILE_BACKUP): + TARGET_FILE.write_text(TARGET_FILE_BACKUP.read_text()) + return True + return False + + +def readfile(file: pathlib.Path = TARGET_FILE) -> bool: try: - content = TARGET_FILE.read_text() + content = file.read_text() except: # noqa: E722 - return - TARGET_FILE_BACKUP.write_text(content) + return False for line in content.splitlines(): key, value = line.split("=", maxsplit=2) data[key] = float(value) + if "time" in data: + TARGET_FILE_BACKUP.write_text(content) + return "time" in data -def main(): - if TARGET_FILE_BACKUP.exists() and TARGET_FILE.exists(): - TARGET_FILE.write_text(TARGET_FILE_BACKUP.read_text()) +def open_device() -> device: # Pin definitions: I2C Bus 1 (SDA=GPIO2, SCL=GPIO3) # Default SSD1306 I2C address is 0x3C serial_interface = i2c(port=1, address=0x3C) try: # Initialize the device. If using SH1106, change ssd1306 to sh1106 - device = sh1106(serial_interface, width=128, height=64, rotate=0) + return sh1106(serial_interface, width=128, height=64, rotate=2) except DeviceNotFoundError: print("Fatal: OLED not found at 0x3C. Check wiring and run i2cdetect -y 1") sys.exit(1) @@ -49,13 +75,106 @@ def main(): print(f"Fatal OS Error: {e}") sys.exit(1) + +def print_loading(restored: bool, t0: float) -> Image.Image: + im = loading_img.copy() + draw = ImageDraw.Draw(im) + + now = time.time() + + percent = min(1.0, (now - t0) / ESTIMATED_BOOT_TIME_SECONDS) + + draw.rectangle((24, 35, 24 + 80 * percent, 40), fill="white") + + if not restored: + draw.circle((4, 4), 1, fill="white") + + return im + + +def print_monitor() -> Image.Image: + im = background_img.copy() + draw = ImageDraw.Draw(im) + + im_overlay = blank_im.copy() + draw2 = ImageDraw.Draw(im_overlay) + + # tempo + draw.text((42, -2), f"{int(data['tempo'])}", fill="white", font=font1) + draw.rectangle( + (6, 7, 6 + 53 * (data["beats"] - int(data["beats"])), 10), fill="white", width=0 + ) + + # time + draw.text((94, -2), f"{int(data['time'])}", fill="white", font=font1) + draw.rectangle( + (67, 7, 67 + 53 * (data["time"] - int(data["time"])), 10), fill="white", width=0 + ) + + # page + draw.circle((55 + 8 * int(data["page"]), 61), 2, fill="white") + + # selected + selected = int(data["selected"]) + draw.rectangle(selected_bounds[selected], outline="white", fill=None) + + # TODO active fx/src + + # fx/mix values + # todo random and time based + fxa_value = 1 - data['value_18_x'] / 127 + draw.rectangle((41, 16 + round(14 * fxa_value), 55, 30), fill="white") + fxb_value = 1 - data['value_25_x'] / 127 + draw.rectangle((41, 40 + round(14 * fxb_value), 55, 54), fill="white") + mfx_value = 1 - data['value_32_x'] / 127 + draw.rectangle((99, 28 + round(14 * mfx_value), 113, 42), fill="white") + mix_value = 1 - data['value_34_x'] / 127 + draw.rectangle((70, 28 + round(14 * mix_value), 84, 42), fill="white") + + # active shaders + draw2.text((16, 20), hex(int(data["state_0"]))[2].upper(), fill="white", font=font2) + draw2.text((16, 44), hex(int(data["state_1"]))[2].upper(), fill="white", font=font2) + draw2.text((45, 20), hex(int(data["state_2"]))[2].upper(), fill="white", font=font2) + draw2.text((45, 44), hex(int(data["state_3"]))[2].upper(), fill="white", font=font2) + draw2.text( + (103, 32), hex(int(data["state_6"]))[2].upper(), fill="white", font=font2 + ) + mix_type = data['value_33_x'] == 0 + draw2.text((74, 32), "M" if mix_type else "K", fill="white", font=font2) # TODO + + return ImageMath.lambda_eval(lambda args: args["convert"](args["a"] ^ args["b"], "1"), a=im, b=im_overlay) + + +def main(): + restored = try_restore_backup() or readfile() + + device = open_device() + + initial = True + started = False + + t0 = time.time() + try: + device.display(print_loading(restored, t0)) + while True: - readfile() - with canvas(device) as draw: - # draw.rectangle((0, 50, 127, 60), outline="white", fill="black") - draw.text((0, 0), f"time={data['time']}", fill="white") - draw.text((0, 10), f"beats={data['beats']}", fill="white") + if not readfile(): + time.sleep(0.01) + continue + + if initial or started: + if not started: + started = "time" in data and data["time"] == 0 + if started: + initial = False + device.display(print_loading(restored, t0)) + else: + device.display(print_monitor()) + + if started and data["time"] > 0: + initial = False + started = False except KeyboardInterrupt: print("Exiting gracefully...") except OSError as e: diff --git a/i2c_background.png b/i2c_background.png new file mode 100644 index 0000000..2b09675 Binary files /dev/null and b/i2c_background.png differ diff --git a/i2c_loading.png b/i2c_loading.png new file mode 100644 index 0000000..f9e3d33 Binary files /dev/null and b/i2c_loading.png differ