feat: work to i2c screen (wip)

This commit is contained in:
2026-10-30 00:04:35 +01:00
parent 0fd6774800
commit b8c030d711
10 changed files with 243 additions and 16 deletions
+1
View File
@@ -1,3 +1,4 @@
error.glsl
forge_default_save.txt
build
tmp*
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+1 -1
View File
@@ -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:
Binary file not shown.
+1
View File
@@ -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
BIN
View File
Binary file not shown.
+106
View File
@@ -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")
+134 -15
View File
@@ -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:
Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B