191 lines
5.7 KiB
Python
Executable File
191 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# see https://electricalflux.com/mcu-raspberry/i2c-oled-display-raspberry-pi-5-wiring-code
|
|
|
|
import collections
|
|
import pathlib
|
|
import sys
|
|
import time
|
|
|
|
from luma.core.error import DeviceNotFoundError
|
|
from luma.core.interface.serial import i2c
|
|
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)
|
|
|
|
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 = file.read_text()
|
|
except: # noqa: E722
|
|
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 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
|
|
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)
|
|
except OSError as e:
|
|
if e.errno == 121:
|
|
print(
|
|
"Fatal: Remote I/O error. I2C bus is locked or physically disconnected."
|
|
)
|
|
else:
|
|
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:
|
|
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:
|
|
if e.errno == 121:
|
|
print("Runtime Error: I2C bus dropped. Check physical connections.")
|
|
device.cleanup()
|
|
sys.exit(1)
|
|
finally:
|
|
device.cleanup()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|