refactor: cleaner python project

This commit is contained in:
2026-11-02 00:41:59 +01:00
parent b8c030d711
commit 009dd8dcb3
21 changed files with 588 additions and 301 deletions
+2
View File
@@ -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

+2 -2
View File
@@ -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
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
from src.forge_invar import main
if __name__ == "__main__":
main()
-106
View File
@@ -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")
-190
View File
@@ -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
View File
@@ -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

+130
View File
@@ -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
+40
View File
@@ -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))
+37
View File
@@ -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),
]
+167
View File
@@ -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")
+8
View File
@@ -0,0 +1,8 @@
import enum
class ForgeState(enum.Enum):
INITIAL = enum.auto()
RESTORED = enum.auto()
STARTED = enum.auto()
RUNNING = enum.auto()
+132
View File
@@ -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
+49
View File
@@ -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
Generated
+2 -2
View File
@@ -44,8 +44,8 @@ wheels = [
[[package]]
name = "forge-invar"
version = "0.0.0"
source = { virtual = "." }
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "luma-oled" },
]