refactor: cleaner python project
This commit is contained in:
@@ -2,3 +2,5 @@ error.glsl
|
||||
forge_default_save.txt
|
||||
build
|
||||
tmp*
|
||||
debug.png
|
||||
forge_default_save.txt.bck
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
@@ -53,7 +53,7 @@ setup-services:
|
||||
|
||||
.PHONY: setup-bashrc
|
||||
setup-bashrc:
|
||||
$(PROJECT_DIR)/line_in_file.sh "make -C $(PROJECT_DIR) --no-print-directory start" "$(HOME)/.bashrc"
|
||||
$(PROJECT_DIR)/scripts/line_in_file.sh "make -C $(PROJECT_DIR) --no-print-directory start" "$(HOME)/.bashrc"
|
||||
|
||||
.PHONY: install
|
||||
install: remove-passwd apt-install pip-install setup-weston remove-motd disable-cloud-init setup-services setup-bashrc
|
||||
@@ -71,7 +71,7 @@ run: $(BUILD_DIR)/$(TARGET)
|
||||
|
||||
.PHONY: start-bg
|
||||
start-bg:
|
||||
@python3 $(PROJECT_DIR)/i2c.py >> $(PROJECT_DIR)/python.log&
|
||||
@python3 $(PROJECT_DIR)/__main__.py &> $(PROJECT_DIR)/python.log &
|
||||
|
||||
.PHONY: start-fg
|
||||
start-fg:
|
||||
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from src.forge_invar import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,106 +0,0 @@
|
||||
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")
|
||||
@@ -1,190 +0,0 @@
|
||||
#!/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()
|
||||
+13
-1
@@ -1,11 +1,23 @@
|
||||
[project]
|
||||
name = "forge-invar"
|
||||
version = "0.0.0"
|
||||
version = "0.1.0"
|
||||
description = "FORGE invar background process for Raspberry Pi"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "klemek", email = "klemek.dev@proton.me" }
|
||||
]
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"luma-oled>=3.16.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
forge-invar = "forge_invar:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.13.0,<0.14.0"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.17.3",
|
||||
|
||||
|
Before Width: | Height: | Size: 301 B After Width: | Height: | Size: 301 B |
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
@@ -0,0 +1,130 @@
|
||||
time=226.380635
|
||||
beats=175.725006
|
||||
demo=0
|
||||
auto_random=0
|
||||
auto_random_cycle=4
|
||||
tempo=60
|
||||
page=0
|
||||
selected=0
|
||||
seed_0=898
|
||||
state_0=0
|
||||
seed_1=472
|
||||
state_1=9
|
||||
seed_2=243
|
||||
state_2=3
|
||||
seed_3=547
|
||||
state_3=14
|
||||
seed_4=524
|
||||
state_4=13
|
||||
seed_5=251
|
||||
state_5=10
|
||||
seed_6=808
|
||||
state_6=4
|
||||
active_0=0
|
||||
active_1=0
|
||||
active_2=0
|
||||
value_0_x=127
|
||||
value_0_y=0
|
||||
value_0_z=0
|
||||
value_1_x=111
|
||||
value_1_y=100
|
||||
value_1_z=71
|
||||
value_2_x=0
|
||||
value_2_y=127
|
||||
value_2_z=127
|
||||
value_3_x=56
|
||||
value_3_y=19
|
||||
value_3_z=71
|
||||
value_4_x=0
|
||||
value_4_y=0
|
||||
value_4_z=127
|
||||
value_5_x=38
|
||||
value_5_y=116
|
||||
value_5_z=82
|
||||
value_6_x=127
|
||||
value_6_y=127
|
||||
value_6_z=127
|
||||
value_7_x=32
|
||||
value_7_y=114
|
||||
value_7_z=113
|
||||
value_8_x=127
|
||||
value_8_y=0
|
||||
value_8_z=127
|
||||
value_9_x=83
|
||||
value_9_y=93
|
||||
value_9_z=62
|
||||
value_10_x=127
|
||||
value_10_y=127
|
||||
value_10_z=0
|
||||
value_11_x=92
|
||||
value_11_y=121
|
||||
value_11_z=1
|
||||
value_12_x=0
|
||||
value_12_y=127
|
||||
value_12_z=127
|
||||
value_13_x=20
|
||||
value_13_y=105
|
||||
value_13_z=64
|
||||
value_14_x=127
|
||||
value_14_y=0
|
||||
value_14_z=127
|
||||
value_15_x=78
|
||||
value_15_y=115
|
||||
value_15_z=28
|
||||
value_16_x=127
|
||||
value_16_y=127
|
||||
value_16_z=127
|
||||
value_17_x=89
|
||||
value_17_y=114
|
||||
value_17_z=115
|
||||
value_18_x=22
|
||||
value_18_y=112
|
||||
value_18_z=0
|
||||
value_19_x=0
|
||||
value_19_y=0
|
||||
value_19_z=127
|
||||
value_20_x=33
|
||||
value_20_y=102
|
||||
value_20_z=121
|
||||
value_21_x=0
|
||||
value_21_y=0
|
||||
value_21_z=127
|
||||
value_22_x=34
|
||||
value_22_y=117
|
||||
value_22_z=92
|
||||
value_23_x=127
|
||||
value_23_y=127
|
||||
value_23_z=127
|
||||
value_24_x=24
|
||||
value_24_y=1
|
||||
value_24_z=103
|
||||
value_25_x=62
|
||||
value_25_y=43
|
||||
value_25_z=127
|
||||
value_26_x=127
|
||||
value_26_y=127
|
||||
value_26_z=127
|
||||
value_27_x=62
|
||||
value_27_y=28
|
||||
value_27_z=88
|
||||
value_28_x=127
|
||||
value_28_y=0
|
||||
value_28_z=127
|
||||
value_29_x=76
|
||||
value_29_y=64
|
||||
value_29_z=8
|
||||
value_30_x=127
|
||||
value_30_y=0
|
||||
value_30_z=0
|
||||
value_31_x=74
|
||||
value_31_y=76
|
||||
value_31_z=10
|
||||
value_32_x=11
|
||||
value_32_y=67
|
||||
value_32_z=0
|
||||
value_33_x=127
|
||||
value_33_y=0
|
||||
value_33_z=0
|
||||
value_34_x=103
|
||||
value_34_y=57
|
||||
value_34_z=0
|
||||
@@ -0,0 +1,40 @@
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from .display import (
|
||||
debug_image,
|
||||
generate_background_image,
|
||||
generate_loading_image,
|
||||
get_display,
|
||||
)
|
||||
from .forge_data import ForgeData
|
||||
from .i2c import I2C
|
||||
|
||||
|
||||
def host_generate() -> None:
|
||||
data = ForgeData().mock()
|
||||
generate_loading_image()
|
||||
generate_background_image()
|
||||
image = get_display(data, 0)
|
||||
debug_image(image)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--generate", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.generate:
|
||||
host_generate()
|
||||
return
|
||||
|
||||
t0 = time.time()
|
||||
with ForgeData() as data, I2C() as device:
|
||||
device.update_display(get_display(data, t0))
|
||||
|
||||
while True:
|
||||
if not data.update():
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
device.update_display(get_display(data, t0))
|
||||
@@ -0,0 +1,37 @@
|
||||
import pathlib
|
||||
|
||||
PROJECT_PATH = pathlib.Path(__file__).parent.parent.parent
|
||||
RESOURCES_PATH = PROJECT_PATH / "resources"
|
||||
|
||||
FORGE_SAVE_PATH = PROJECT_PATH / "forge_default_save.txt"
|
||||
FORGE_SAVE_BACKUP_PATH = PROJECT_PATH / "forge_default_save.txt.bck"
|
||||
MOCK_SAVE_PATH = RESOURCES_PATH / "mock_data.txt"
|
||||
|
||||
|
||||
FONT_1_PATH = RESOURCES_PATH / "TinyAndChunkyRegular.ttf"
|
||||
FONT_2_PATH = RESOURCES_PATH / "Px437_IBM_BIOS.ttf"
|
||||
|
||||
LOADING_IMG_PATH = RESOURCES_PATH / "loading.png"
|
||||
BACKGROUND_IMG_PATH = RESOURCES_PATH / "background.png"
|
||||
|
||||
ESTIMATED_BOOT_TIME_SECONDS = 18.0
|
||||
|
||||
SELECTED_RECT_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),
|
||||
]
|
||||
|
||||
ACTIVE_SRC_RECT_BOUNDS = [
|
||||
(10, 32, 28, 32),
|
||||
(10, 56, 28, 56),
|
||||
]
|
||||
|
||||
ACTIVE_FX_RECT_BOUNDS = [
|
||||
(39, 32, 57, 32),
|
||||
(39, 56, 57, 56),
|
||||
(97, 44, 115, 44),
|
||||
]
|
||||
@@ -0,0 +1,167 @@
|
||||
import time
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageMath
|
||||
|
||||
from .constants import (
|
||||
ACTIVE_FX_RECT_BOUNDS,
|
||||
ACTIVE_SRC_RECT_BOUNDS,
|
||||
BACKGROUND_IMG_PATH,
|
||||
ESTIMATED_BOOT_TIME_SECONDS,
|
||||
FONT_1_PATH,
|
||||
FONT_2_PATH,
|
||||
LOADING_IMG_PATH,
|
||||
SELECTED_RECT_BOUNDS,
|
||||
)
|
||||
from .enums import ForgeState
|
||||
from .forge_data import ForgeData
|
||||
|
||||
FONT_1 = ImageFont.truetype(FONT_1_PATH, 5)
|
||||
FONT_2 = ImageFont.truetype(FONT_2_PATH, 8)
|
||||
LOADING_IMG = Image.open(LOADING_IMG_PATH)
|
||||
BACKGROUND_IMG = Image.open(BACKGROUND_IMG_PATH)
|
||||
BLANK_IMG = Image.new("1", (128, 64), "black")
|
||||
|
||||
|
||||
def __xor(img1: Image.Image, img2: Image.Image) -> Image.Image:
|
||||
return ImageMath.lambda_eval(
|
||||
lambda args: args["convert"](args["a"] ^ args["b"], "1"), a=img1, b=img2
|
||||
)
|
||||
|
||||
|
||||
def __get_loading_display(data: ForgeData, 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 data.state == ForgeState.INITIAL:
|
||||
draw.circle((4, 4), 1, fill="white")
|
||||
|
||||
return im
|
||||
|
||||
|
||||
def __get_running_display(data: ForgeData) -> Image.Image:
|
||||
im = BACKGROUND_IMG.copy()
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
im_overlay = BLANK_IMG.copy()
|
||||
draw2 = ImageDraw.Draw(im_overlay)
|
||||
|
||||
# tempo
|
||||
draw.text((42, -2), f"{int(data.tempo)}", fill="white", font=FONT_1)
|
||||
draw.rectangle((6, 7, 6 + int(53 * data.beat_progress), 10), fill="white", width=0)
|
||||
|
||||
# time
|
||||
draw.text((94, -2), f"{int(data.time)}", fill="white", font=FONT_1)
|
||||
draw.rectangle(
|
||||
(67, 7, 67 + int(53 * data.time_progress), 10), fill="white", width=0
|
||||
)
|
||||
|
||||
# page
|
||||
draw.circle((55 + 8 * data.page, 61), 2, fill="white")
|
||||
|
||||
# selected
|
||||
draw.rectangle(SELECTED_RECT_BOUNDS[data.selected], outline="white", fill=None)
|
||||
|
||||
# active
|
||||
draw.rectangle(ACTIVE_SRC_RECT_BOUNDS[data.active(0)], fill="white")
|
||||
draw.rectangle(ACTIVE_FX_RECT_BOUNDS[data.active(1)], fill="white")
|
||||
|
||||
# fx/mix values
|
||||
draw.rectangle((41, 16 + round(14 * (1 - data.fxa_value)), 55, 30), fill="white")
|
||||
draw.rectangle((41, 40 + round(14 * (1 - data.fxb_value)), 55, 54), fill="white")
|
||||
draw.rectangle((99, 28 + round(14 * (1 - data.mfx_value)), 113, 42), fill="white")
|
||||
draw.rectangle((70, 28 + round(14 * (1 - data.mix_value)), 84, 42), fill="white")
|
||||
|
||||
# active shaders
|
||||
draw2.text((16, 20), hex(data.frag_state(0))[2].upper(), fill="white", font=FONT_2)
|
||||
draw2.text((16, 44), hex(data.frag_state(1))[2].upper(), fill="white", font=FONT_2)
|
||||
draw2.text((45, 20), hex(data.frag_state(2))[2].upper(), fill="white", font=FONT_2)
|
||||
draw2.text((45, 44), hex(data.frag_state(3))[2].upper(), fill="white", font=FONT_2)
|
||||
draw2.text((103, 32), hex(data.frag_state(5))[2].upper(), fill="white", font=FONT_2)
|
||||
# TODO print when random
|
||||
|
||||
draw2.text((74, 32), "K" if data.mix_mode_key else "M", fill="white", font=FONT_2)
|
||||
|
||||
# TODO show feedback
|
||||
|
||||
return __xor(im, im_overlay)
|
||||
|
||||
|
||||
def get_display(data: ForgeData, t0: float) -> Image.Image:
|
||||
if data.state == ForgeState.RUNNING:
|
||||
return __get_running_display(data)
|
||||
return __get_loading_display(data, t0)
|
||||
|
||||
|
||||
def generate_loading_image() -> None:
|
||||
global LOADING_IMG
|
||||
|
||||
im = BLANK_IMG.copy()
|
||||
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
draw.text((26, 24), "F.O.R.G.E.", fill="white", font=FONT_2)
|
||||
draw.rectangle((24, 35, 104, 40), outline="white", fill="black")
|
||||
|
||||
im.save(LOADING_IMG_PATH)
|
||||
|
||||
LOADING_IMG = im
|
||||
|
||||
|
||||
def generate_background_image() -> None:
|
||||
global BACKGROUND_IMG
|
||||
|
||||
im = BLANK_IMG.copy()
|
||||
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# tempo
|
||||
draw.text((7, -2), "TEMPO:", fill="white", font=FONT_1)
|
||||
draw.rectangle((6, 6, 60, 11), outline="white", fill="black")
|
||||
|
||||
# time
|
||||
draw.text((68, -2), "TIME:", fill="white", font=FONT_1)
|
||||
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")
|
||||
|
||||
# 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(BACKGROUND_IMG_PATH)
|
||||
|
||||
BACKGROUND_IMG = im
|
||||
|
||||
|
||||
def debug_image(im: Image.Image) -> None:
|
||||
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("debug.png")
|
||||
@@ -0,0 +1,8 @@
|
||||
import enum
|
||||
|
||||
|
||||
class ForgeState(enum.Enum):
|
||||
INITIAL = enum.auto()
|
||||
RESTORED = enum.auto()
|
||||
STARTED = enum.auto()
|
||||
RUNNING = enum.auto()
|
||||
@@ -0,0 +1,132 @@
|
||||
import collections
|
||||
import pathlib
|
||||
import typing
|
||||
|
||||
from .constants import FORGE_SAVE_BACKUP_PATH, FORGE_SAVE_PATH, MOCK_SAVE_PATH
|
||||
from .enums import ForgeState
|
||||
|
||||
|
||||
class ForgeRawData:
|
||||
def __init__(self) -> None:
|
||||
self.__data: dict[str, float] = collections.defaultdict(lambda: 0)
|
||||
|
||||
def try_restore_backup(self) -> bool:
|
||||
if FORGE_SAVE_BACKUP_PATH.exists() and self.read(FORGE_SAVE_BACKUP_PATH):
|
||||
FORGE_SAVE_PATH.write_text(FORGE_SAVE_BACKUP_PATH.read_text())
|
||||
return True
|
||||
return False
|
||||
|
||||
def read(self, file: pathlib.Path = FORGE_SAVE_PATH) -> bool:
|
||||
try:
|
||||
content = file.read_text()
|
||||
except: # noqa: E722
|
||||
return False
|
||||
for line in content.splitlines():
|
||||
key, value = line.split("=", maxsplit=2)
|
||||
self.__data[key] = float(value)
|
||||
if "time" in self.__data:
|
||||
FORGE_SAVE_BACKUP_PATH.write_text(content)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __getitem__(self, key: str) -> float:
|
||||
return self.__data[key]
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self.__data
|
||||
|
||||
|
||||
class ForgeData:
|
||||
def __init__(self) -> None:
|
||||
self.__data = ForgeRawData()
|
||||
self.state = ForgeState.INITIAL
|
||||
|
||||
def __enter__(self) -> typing.Self:
|
||||
if self.__data.try_restore_backup() or self.__data.read():
|
||||
self.state = ForgeState.RESTORED
|
||||
return self
|
||||
|
||||
def __exit__(self, *_) -> None:
|
||||
pass
|
||||
|
||||
def __update_state(self) -> None:
|
||||
if self.state == ForgeState.RUNNING:
|
||||
return
|
||||
elif self.state == ForgeState.STARTED:
|
||||
if self.time > 0:
|
||||
self.state = ForgeState.RUNNING
|
||||
elif "time" in self.__data and self.time == 0:
|
||||
self.state = ForgeState.STARTED
|
||||
|
||||
def update(self) -> bool:
|
||||
if self.__data.read():
|
||||
self.__update_state()
|
||||
return True
|
||||
return False
|
||||
|
||||
def value(self, n: int) -> tuple[float, float, float]:
|
||||
return (
|
||||
self.__data[f"value_{n}_x"] / 127,
|
||||
self.__data[f"value_{n}_y"] / 127,
|
||||
self.__data[f"value_{n}_z"] / 127,
|
||||
)
|
||||
|
||||
def frag_state(self, n: int) -> int:
|
||||
return int(self.__data[f"state_{n}"])
|
||||
|
||||
def active(self, n: int) -> int:
|
||||
return int(self.__data[f"active_{n}"])
|
||||
|
||||
@property
|
||||
def time(self) -> float:
|
||||
return self.__data["time"]
|
||||
|
||||
@property
|
||||
def time_progress(self) -> float:
|
||||
return self.time - int(self.time)
|
||||
|
||||
@property
|
||||
def tempo(self) -> float:
|
||||
return self.__data["tempo"]
|
||||
|
||||
@property
|
||||
def beats(self) -> float:
|
||||
return self.__data["beats"]
|
||||
|
||||
@property
|
||||
def beat_progress(self) -> float:
|
||||
return self.beats - int(self.beats)
|
||||
|
||||
@property
|
||||
def page(self) -> int:
|
||||
return int(self.__data["page"])
|
||||
|
||||
@property
|
||||
def selected(self) -> int:
|
||||
return int(self.__data["selected"])
|
||||
|
||||
@property
|
||||
def fxa_value(self) -> float:
|
||||
# todo random and time based
|
||||
return self.value(18)[0]
|
||||
|
||||
@property
|
||||
def fxb_value(self) -> float:
|
||||
return self.value(25)[0]
|
||||
|
||||
@property
|
||||
def mfx_value(self) -> float:
|
||||
return self.value(32)[0]
|
||||
|
||||
@property
|
||||
def mix_value(self) -> float:
|
||||
return self.value(34)[0]
|
||||
|
||||
@property
|
||||
def mix_mode_key(self) -> bool:
|
||||
return self.value(33)[0] > 0.
|
||||
|
||||
def mock(self) -> typing.Self:
|
||||
self.__data.read(MOCK_SAVE_PATH)
|
||||
self.state = ForgeState.RUNNING
|
||||
return self
|
||||
@@ -0,0 +1,49 @@
|
||||
import sys
|
||||
import typing
|
||||
|
||||
from luma.core.error import DeviceNotFoundError
|
||||
from luma.core.interface.serial import i2c
|
||||
from luma.oled.device import device, sh1106
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class I2C:
|
||||
def __init__(self) -> None:
|
||||
self.__device: device | None = None
|
||||
|
||||
def __enter__(self) -> typing.Self:
|
||||
self.__open_device()
|
||||
return self
|
||||
|
||||
def __exit__(self, *_) -> None:
|
||||
assert self.__device is not None
|
||||
self.__device.cleanup()
|
||||
|
||||
def __open_device(self) -> None:
|
||||
# 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
|
||||
self.__device = sh1106(serial_interface, width=128, height=64, rotate=0)
|
||||
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 update_display(self, image: Image.Image) -> None:
|
||||
assert self.__device is not None
|
||||
try:
|
||||
self.__device.display(image)
|
||||
except OSError as e:
|
||||
if e.errno == 121:
|
||||
print("Runtime Error: I2C bus dropped. Check physical connections.")
|
||||
raise
|
||||
Reference in New Issue
Block a user