aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-01-24 02:31:52 -0800
committerPinapelz <yukais@pinapelz.com>2025-01-24 02:31:52 -0800
commit5d488d56434f486b42dae04d5326a5a81106ad31 (patch)
tree22aef0b29bfaa09e2bed4a8c13b64efc7a21ff0a
initial commit1.0
-rw-r--r--.gitignore175
-rw-r--r--README.md14
-rw-r--r--chuni-hands-evolved.py649
-rw-r--r--chuniio.py35
4 files changed, 873 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..38c5a0b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,175 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+config.json \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..da90a2b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# chuni-hands-evolved
+Another mysterious tool that detects things. Inspired by [chuni-hands](https://github.com/logchan/chuni-hands)
+- Can output both keybinds or use Brokenithm-Evolved's chuniio
+
+![](https://files.catbox.moe/b2r4ae.png)
+
+# Usage Notes
+In most cases you will not need to turn on keybinds output option (unless you are doing something custom)
+
+If you are using this with UMIGURI and are using [brokenithm-evolved-ios-umi](https://git.moekyun.me/pinapelz/brokenithm-evolved-ios-umi), keybinds option is not needed.
+
+The UI will lag when the update rate is set to low, its recommended that you first configure using a high update rate, then set it as low as possible for use to ensure minimal latency.
+
+*It takes some time to start up, so please be patient*. \ No newline at end of file
diff --git a/chuni-hands-evolved.py b/chuni-hands-evolved.py
new file mode 100644
index 0000000..0dee3fd
--- /dev/null
+++ b/chuni-hands-evolved.py
@@ -0,0 +1,649 @@
+import cv2
+import tkinter as tk
+from tkinter import Scale, StringVar, Button, Checkbutton, BooleanVar, messagebox
+import chuniio
+from PIL import Image, ImageTk
+import numpy as np
+import keyboard # Requires `keyboard` library
+import json
+
+zones = [
+ {"x": 300, "y": 100, "width": 50, "height": 50},
+ {"x": 300, "y": 200, "width": 50, "height": 50},
+ {"x": 300, "y": 300, "width": 50, "height": 50},
+ {"x": 300, "y": 400, "width": 50, "height": 50},
+ {"x": 300, "y": 500, "width": 50, "height": 50},
+ {"x": 300, "y": 600, "width": 50, "height": 50},
+]
+
+_UMIGIRI_32_AIRZONE_LAYOUT = {
+ 0: "o",
+ 1: "0",
+ 2: "p",
+ 3: "l",
+ 4: ",",
+ 5: ".",
+}
+
+base_positions = [{"x": zone["x"], "y": zone["y"]} for zone in zones]
+zone_color_state = []
+CONFIG_FILE = "config.json"
+CAMERA_WIDTH = 1280
+CAMERA_HEIGHT = 720
+ZONE_TRIGGERED_STATE = [False] * len(zones)
+
+
+def get_avg_brightness(frame, zone):
+ x, y, w, h = zone["x"], zone["y"], zone["width"], zone["height"]
+ roi = frame[y : y + h, x : x + w]
+ if roi.size == 0:
+ return 0
+ return np.mean(cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY))
+
+
+def get_available_cameras():
+ print("Now testing to see which cameras are available... (disregard the errors here)")
+ available_cameras = []
+ for i in range(10):
+ cap = cv2.VideoCapture(i)
+ if cap.isOpened():
+ available_cameras.append(i)
+ cap.release()
+ return available_cameras
+
+
+def calculate_preview_size(width, height, max_height=720):
+ """Calculate preview dimensions maintaining aspect ratio"""
+ if height <= max_height:
+ return width, height
+
+ aspect_ratio = width / height
+ new_height = max_height
+ new_width = int(aspect_ratio * new_height)
+ return new_width, new_height
+
+
+def setup_gui(
+ camera_width: int, camera_height: int, preview_width: int, preview_height: int
+) -> tk.Tk:
+ root = tk.Tk()
+ root.title("chuni-hands-evolved")
+ root.attributes("-topmost", True)
+
+ screen_width = root.winfo_screenwidth()
+ screen_height = root.winfo_screenheight()
+
+ window_width = min(int(screen_width * 0.8), preview_width + 40)
+ window_height = min(int(screen_height * 0.8), preview_height + 300)
+
+ x = (screen_width - window_width) // 2
+ y = (screen_height - window_height) // 2
+ root.geometry(f"{window_width}x{window_height}+{x}+{y}")
+
+ style = {
+ "bg": "#f0f0f0",
+ "padding": 10,
+ "button_bg": "#4a90e2",
+ "button_fg": "white",
+ "frame_bg": "#ffffff",
+ "label_font": ("Arial", 10),
+ "button_font": ("Arial", 10, "bold"),
+ }
+
+ root.configure(bg=style["bg"])
+
+ main_container = tk.Frame(
+ root, bg=style["bg"], padx=style["padding"], pady=style["padding"]
+ )
+ main_container.pack(
+ fill="both", expand=True, padx=style["padding"], pady=style["padding"]
+ )
+
+ # Video canvas
+ video_frame = tk.Frame(main_container, bg=style["frame_bg"], relief="ridge", bd=2)
+ video_frame.pack(fill="both", expand=True, pady=(0, style["padding"]))
+
+ video_canvas = tk.Canvas(video_frame, width=preview_width, height=preview_height)
+ video_canvas.pack(fill="both", expand=True)
+
+ # Camera selection
+ camera_frame = tk.Frame(
+ main_container, bg=style["frame_bg"], relief="ridge", bd=2, padx=10, pady=10
+ )
+ camera_frame.pack(fill="x", pady=(0, style["padding"]))
+
+ tk.Label(
+ camera_frame,
+ text="Camera Number:",
+ font=style["label_font"],
+ bg=style["frame_bg"],
+ ).pack(side="left")
+
+ available_cameras = get_available_cameras()
+ if not available_cameras:
+ print("No cameras found!")
+ available_cameras = [0]
+
+ camera_var = StringVar()
+ camera_var.set(
+ str(
+ current_camera_index
+ if current_camera_index in available_cameras
+ else available_cameras[0]
+ )
+ )
+ camera_dropdown = tk.OptionMenu(camera_frame, camera_var, *available_cameras)
+ camera_dropdown.configure(width=10)
+ camera_dropdown.pack(side="left", padx=(10, 0))
+
+ tk.Label(
+ camera_frame,
+ text="Update Rate (ms):",
+ font=style["label_font"],
+ bg=style["frame_bg"],
+ ).pack(side="left")
+
+ update_rate = tk.StringVar(value="5")
+ update_rate_spinbox = tk.Spinbox(
+ camera_frame,
+ from_=1,
+ to=100,
+ textvariable=update_rate,
+ width=5,
+ )
+ update_rate_spinbox.pack(side="left", padx=5)
+
+ tk.Label(
+ camera_frame,
+ text="You should set the update rate to something like 100 while editing for a smoother experience, then set it back to 5 for normal use.",
+ font=style["label_font"],
+ bg=style["frame_bg"],
+ ).pack(side="top", pady=(0, style["padding"]))
+
+ controls_frame = tk.Frame(
+ main_container, bg=style["frame_bg"], relief="ridge", bd=2, padx=10, pady=10
+ )
+ controls_frame.pack(fill="x", pady=(0, style["padding"]))
+
+ inputs_frame = tk.Frame(controls_frame, bg=style["frame_bg"])
+ inputs_frame.pack(fill="x")
+
+ x_frame = tk.Frame(inputs_frame, bg=style["frame_bg"])
+ x_frame.pack(fill="x", pady=(0, 5))
+ tk.Label(
+ x_frame,
+ text="X Position:",
+ font=style["label_font"],
+ bg=style["frame_bg"],
+ width=15,
+ ).pack(side="left")
+
+ x_var = tk.StringVar(value="0")
+ x_spinbox = tk.Spinbox(
+ x_frame,
+ from_=-500,
+ to=1280,
+ textvariable=x_var,
+ increment=10,
+ width=10,
+ command=lambda: update_position("x"),
+ )
+ x_spinbox.pack(side="left", padx=5)
+
+ x_slider = Scale(
+ x_frame,
+ from_=-500,
+ to=1280,
+ orient="horizontal",
+ length=400,
+ showvalue=0,
+ command=lambda v: x_var.set(str(int(float(v)))),
+ )
+ x_slider.pack(side="left", fill="x", expand=True, padx=5)
+
+ y_frame = tk.Frame(inputs_frame, bg=style["frame_bg"])
+ y_frame.pack(fill="x", pady=(0, 5))
+ tk.Label(
+ y_frame,
+ text="Y Position:",
+ font=style["label_font"],
+ bg=style["frame_bg"],
+ width=15,
+ ).pack(side="left")
+
+ y_var = tk.StringVar(value="0")
+ y_spinbox = tk.Spinbox(
+ y_frame,
+ from_=-200,
+ to=720,
+ textvariable=y_var,
+ increment=10,
+ width=10,
+ command=lambda: update_position("y"),
+ )
+ y_spinbox.pack(side="left", padx=5)
+
+ y_slider = Scale(
+ y_frame,
+ from_=-200,
+ to=720,
+ orient="horizontal",
+ length=400,
+ showvalue=0,
+ command=lambda v: y_var.set(str(int(float(v)))),
+ )
+ y_slider.pack(side="left", fill="x", expand=True, padx=5)
+
+ # Spacing controls
+ spacing_frame = tk.Frame(inputs_frame, bg=style["frame_bg"])
+ spacing_frame.pack(fill="x")
+ tk.Label(
+ spacing_frame,
+ text="Sensor Spacing:",
+ font=style["label_font"],
+ bg=style["frame_bg"],
+ width=15,
+ ).pack(side="left")
+
+ spacing_var = tk.StringVar(value="100")
+ spacing_spinbox = tk.Spinbox(
+ spacing_frame,
+ from_=10,
+ to=300,
+ textvariable=spacing_var,
+ increment=5,
+ width=10,
+ command=lambda: update_position("spacing"),
+ )
+ spacing_spinbox.pack(side="left", padx=5)
+
+ spacing_slider = Scale(
+ spacing_frame,
+ from_=10,
+ to=300,
+ orient="horizontal",
+ length=400,
+ showvalue=0,
+ command=lambda v: spacing_var.set(str(int(float(v)))),
+ )
+ spacing_slider.pack(side="left", fill="x", expand=True, padx=5)
+
+ width_frame = tk.Frame(inputs_frame, bg=style["frame_bg"])
+ width_frame.pack(fill="x")
+ tk.Label(
+ width_frame,
+ text="Sensor Width:",
+ font=style["label_font"],
+ bg=style["frame_bg"],
+ width=15,
+ ).pack(side="left")
+
+ width_var = tk.StringVar(value="50")
+ width_spinbox = tk.Spinbox(
+ width_frame,
+ from_=10,
+ to=1000,
+ textvariable=width_var,
+ increment=5,
+ width=10,
+ command=lambda: update_width(),
+ )
+ width_spinbox.pack(side="left", padx=5)
+
+ def update_width():
+ try:
+ value = int(width_var.get())
+ if 10 <= value <= 1000:
+ for zone in zones:
+ zone["width"] = value
+ except ValueError:
+ pass
+
+ def update_position(type):
+ try:
+ if type == "x":
+ value = int(x_var.get())
+ if -500 <= value <= 1280:
+ x_slider.set(value)
+ elif type == "y":
+ value = int(y_var.get())
+ if -200 <= value <= 720:
+ y_slider.set(value)
+ elif type == "spacing":
+ value = int(spacing_var.get())
+ if 10 <= value <= 300:
+ spacing_slider.set(value)
+ except ValueError:
+ pass
+
+ def on_mousewheel(event, spinbox):
+ if event.delta > 0:
+ spinbox.invoke("buttonup")
+ else:
+ spinbox.invoke("buttondown")
+
+ x_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, x_spinbox))
+ y_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, y_spinbox))
+ spacing_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, spacing_spinbox))
+ width_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, width_spinbox))
+
+ buttons_frame = tk.Frame(
+ main_container, bg=style["frame_bg"], relief="ridge", bd=2, padx=10, pady=10
+ )
+ buttons_frame.pack(fill="x")
+
+ def recalibrate():
+ ret, frame = cap.read()
+ if ret:
+ calibrate(frame)
+ print("Recalibration complete.")
+ else:
+ print("Error: Could not capture frame for recalibration.")
+
+ def save_config():
+ config = {
+ "x_offset": x_slider.get(),
+ "y_offset": y_slider.get(),
+ "spacing": spacing_slider.get(),
+ "width": int(width_var.get()),
+ "camera_index": current_camera_index,
+ "chuniio_enabled": chuniio_enabled.get(),
+ }
+ with open(CONFIG_FILE, "w") as f:
+ json.dump(config, f)
+ messagebox.showinfo("Config", "Configuration saved successfully.")
+
+ button_kwargs = {
+ "bg": style["button_bg"],
+ "fg": style["button_fg"],
+ "font": style["button_font"],
+ "relief": "flat",
+ "padx": 20,
+ "pady": 5,
+ }
+
+ calibration_button = Button(
+ buttons_frame, text="Recalibrate", command=recalibrate, **button_kwargs
+ )
+ calibration_button.pack(side="left", padx=5)
+
+ save_button = Button(
+ buttons_frame, text="Save Config", command=save_config, **button_kwargs
+ )
+ save_button.pack(side="left", padx=5)
+
+ chuniio_enabled = BooleanVar(value=True)
+ chuniio_button = Checkbutton(
+ buttons_frame,
+ text="Brokenithm Evolved chuniio",
+ variable=chuniio_enabled,
+ font=style["button_font"],
+ relief="flat",
+ padx=20,
+ pady=5,
+ bg=style["frame_bg"],
+ )
+ chuniio_button.pack(side="right", padx=5)
+
+ keystrokes_enabled = BooleanVar(value=False)
+ keystroke_button = Button(
+ buttons_frame,
+ text="⌨ Keystrokes: OFF",
+ command=lambda: toggle_keystrokes(),
+ **button_kwargs,
+ )
+ keystroke_button.pack(side="right", padx=5)
+
+ def toggle_keystrokes():
+ current = keystrokes_enabled.get()
+ keystrokes_enabled.set(not current)
+ keystroke_button.config(
+ text="⌨ Keystrokes: ON" if not current else "⌨ Keystrokes: OFF",
+ bg="#4a90e2" if not current else "#e74c3c",
+ )
+
+ def on_window_resize(event):
+ if event.widget == root:
+ new_width = video_frame.winfo_width()
+ new_height = video_frame.winfo_height()
+ aspect_ratio = preview_width / preview_height
+
+ if new_width / new_height > aspect_ratio:
+ canvas_height = new_height
+ canvas_width = int(canvas_height * aspect_ratio)
+ else:
+ canvas_width = new_width
+ canvas_height = int(canvas_width / aspect_ratio)
+
+ video_canvas.configure(width=canvas_width, height=canvas_height)
+
+ root.bind("<Configure>", on_window_resize)
+ controls_frame = tk.Frame(
+ main_container, bg=style["frame_bg"], relief="ridge", bd=2
+ )
+ controls_frame.pack(fill="x", pady=(0, style["padding"]))
+
+ buttons_frame = tk.Frame(main_container, bg=style["frame_bg"], relief="ridge", bd=2)
+ buttons_frame.pack(fill="x")
+ for slider in [x_slider, y_slider, spacing_slider]:
+ slider.pack(fill="x", expand=True, padx=5)
+
+ def switch_camera(camera_width: int, camera_height: int):
+ global cap, current_camera_index
+ current_camera_index = int(camera_var.get())
+ cap.release()
+ cap = cv2.VideoCapture(current_camera_index)
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, camera_width)
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, camera_height)
+ if not cap.isOpened():
+ messagebox.showerror(
+ "Camera Error", "Failed to access the selected camera."
+ )
+
+ camera_var.trace_add(
+ "write",
+ lambda *args: switch_camera(
+ camera_width=camera_width, camera_height=camera_height
+ ),
+ )
+
+ return (
+ root,
+ video_canvas,
+ x_slider,
+ y_slider,
+ width_var,
+ spacing_slider,
+ keystrokes_enabled,
+ chuniio_enabled,
+ update_rate,
+ )
+
+
+def calibrate(frame):
+ global zone_color_state
+ zone_color_state = []
+ for zone in zones:
+ brightness = get_avg_brightness(frame, zone)
+ zone_color_state.append(brightness)
+ print("Calibration completed. Initial lighting values:", zone_color_state)
+
+
+def update_frame(
+ preview_width,
+ preview_height,
+ chuniio_shared_memory,
+):
+ global cap
+ ret, frame = cap.read()
+ if not ret:
+ print("Error: Could not capture frame.")
+ return
+
+ frame = cv2.flip(frame, 1)
+ x_offset = x_slider.get()
+ y_offset = y_slider.get()
+ spacing = spacing_slider.get()
+
+ # Update zone positions
+ for i, zone in enumerate(zones):
+ zone["x"] = base_positions[i]["x"] + x_offset
+ zone["y"] = base_positions[0]["y"] + y_offset + i * spacing
+
+ # Process zones
+ for i, zone in enumerate(zones):
+ current_brightness = get_avg_brightness(frame, zone)
+ if abs(current_brightness - zone_color_state[i]) > 20:
+ if not ZONE_TRIGGERED_STATE[i]:
+ if keystrokes_enabled.get():
+ print(f"Key '{_UMIGIRI_32_AIRZONE_LAYOUT[i]}' pressed.")
+ keyboard.press(_UMIGIRI_32_AIRZONE_LAYOUT[i])
+ print(f"Zone {i} triggered")
+ ZONE_TRIGGERED_STATE[i] = True
+ cv2.rectangle(
+ frame,
+ (zone["x"], zone["y"]),
+ (zone["x"] + zone["width"], zone["y"] + zone["height"]),
+ (0, 255, 0),
+ 3,
+ )
+ else:
+ if ZONE_TRIGGERED_STATE[i]:
+ if keystrokes_enabled.get():
+ keyboard.release(_UMIGIRI_32_AIRZONE_LAYOUT[i])
+ print(f"Key '{_UMIGIRI_32_AIRZONE_LAYOUT[i]}' released.")
+ ZONE_TRIGGERED_STATE[i] = False
+ cv2.rectangle(
+ frame,
+ (zone["x"], zone["y"]),
+ (zone["x"] + zone["width"], zone["y"] + zone["height"]),
+ (0, 0, 255),
+ 2,
+ )
+
+ if chuniio_enabled.get():
+ chuniio.write_to_airzone(ZONE_TRIGGERED_STATE, chuniio_shared_memory)
+
+ # Convert to RGB
+ rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+
+ # Get canvas dimensions
+ canvas_width = video_canvas.winfo_width()
+ canvas_height = video_canvas.winfo_height()
+
+ # Ensure valid dimensions before resizing
+ if canvas_width <= 0 or canvas_height <= 0:
+ canvas_width = preview_width
+ canvas_height = preview_height
+
+ # Calculate aspect ratio preserving dimensions
+ aspect_ratio = frame.shape[1] / frame.shape[0]
+ if canvas_width / canvas_height > aspect_ratio:
+ display_height = canvas_height
+ display_width = int(display_height * aspect_ratio)
+ else:
+ display_width = canvas_width
+ display_height = int(display_width / aspect_ratio)
+
+ # Ensure minimum dimensions
+ display_width = max(display_width, 1)
+ display_height = max(display_height, 1)
+
+ # Resize frame
+ try:
+ rgb_frame = cv2.resize(rgb_frame, (display_width, display_height))
+ except cv2.error as e:
+ print(f"Resize error: {display_width}x{display_height}")
+ return
+
+ # Display frame
+ img = ImageTk.PhotoImage(Image.fromarray(rgb_frame))
+ video_canvas.delete("all")
+ video_canvas.create_image(
+ canvas_width // 2, canvas_height // 2, anchor="center", image=img
+ )
+ video_canvas.image = img
+
+ try:
+ rate = max(1, int(update_rate.get()))
+ except ValueError:
+ rate = 5
+ root.after(rate, update_frame, preview_width, preview_height, chuniio_shared_memory)
+
+
+def load_config() -> dict:
+ try:
+ with open(CONFIG_FILE, "r") as f:
+ return json.load(f)
+ except FileNotFoundError:
+ return {}
+
+
+def main():
+ global \
+ root, \
+ current_camera_index, \
+ keystrokes_enabled, \
+ chuniio_enabled, \
+ video_canvas, \
+ x_slider, \
+ y_slider, \
+ width_var, \
+ spacing_slider, \
+ cap, \
+ update_rate, \
+ CAMERA_WIDTH, \
+ CAMERA_HEIGHT
+ print("Now starting up... please wait a bit...")
+ config = load_config()
+ current_camera_index = config.get("camera_index", 0)
+ cap = cv2.VideoCapture(current_camera_index)
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, CAMERA_WIDTH)
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, CAMERA_HEIGHT)
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
+ cap.set(cv2.CAP_PROP_FPS, 30)
+ preview_width, preview_height = calculate_preview_size(CAMERA_WIDTH, CAMERA_HEIGHT)
+
+ actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
+ actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
+
+ if actual_width != CAMERA_WIDTH or actual_height != CAMERA_HEIGHT:
+ print(
+ f"Resolution {CAMERA_WIDTH}x{CAMERA_HEIGHT} not supported. Using {actual_width}x{actual_height}."
+ )
+ CAMERA_WIDTH = int(actual_width)
+ CAMERA_HEIGHT = int(actual_height)
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, actual_width)
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, actual_height)
+ ret, frame = cap.read()
+ if ret:
+ calibrate(frame)
+ else:
+ print("Error: Could not capture initial frame. Check your camera.")
+
+ (
+ root,
+ video_canvas,
+ x_slider,
+ y_slider,
+ width_var,
+ spacing_slider,
+ keystrokes_enabled,
+ chuniio_enabled,
+ update_rate,
+ ) = setup_gui(CAMERA_WIDTH, CAMERA_HEIGHT, preview_width, preview_height)
+
+ x_slider.set(config.get("x_offset", 0))
+ y_slider.set(config.get("y_offset", 0))
+ spacing_slider.set(config.get("spacing", 100))
+ chuniio_enabled.set(config.get("chuniio_enabled", True))
+ chuniio_shared_memory = chuniio.open_sharedmem() if chuniio_enabled.get() else None
+ width_var.set(str(config.get("width", 50)))
+ for zone in zones:
+ zone["width"] = int(width_var.get())
+ update_frame(preview_width, preview_height, chuniio_shared_memory)
+ root.mainloop()
+ cap.release()
+
+
+main()
diff --git a/chuniio.py b/chuniio.py
new file mode 100644
index 0000000..e851063
--- /dev/null
+++ b/chuniio.py
@@ -0,0 +1,35 @@
+import ctypes
+import mmap
+
+class SharedMemoryData(ctypes.Structure):
+ _fields_ = [
+ ("airIoStatus", ctypes.c_uint8 * 6),
+ ("sliderIoStatus", ctypes.c_uint8 * 32),
+ ("ledRgbData", ctypes.c_uint8 * 96),
+ ("reserved", ctypes.c_uint8 * 4)
+ ]
+
+SHARED_MEMORY_NAME = "Local\\BROKENITHM_SHARED_BUFFER"
+SHARED_MEMORY_SIZE = ctypes.sizeof(ctypes.c_uint8) * ctypes.sizeof(SharedMemoryData)
+
+def open_sharedmem():
+ try:
+ shared_memory = mmap.mmap(-1, SHARED_MEMORY_SIZE,
+ tagname=SHARED_MEMORY_NAME,
+ access=mmap.ACCESS_DEFAULT)
+ return shared_memory
+ except Exception:
+ print("[Error] A Fatal Error occured while trying to write to the Shared Memory while initializing")
+ return None
+
+def write_to_airzone(air_zone_state: list, shared_memory: mmap.mmap):
+ if len(air_zone_state) != 6 and all(isinstance(state, bool) for state in air_zone_state):
+ raise ValueError("air_input must have exactly 6 elements")
+ air_states = bytes([128 if state else 0 for state in fix_air_order(air_zone_state)])
+ shared_memory.seek(0)
+ shared_memory.write(bytes(air_states))
+
+def fix_air_order(air_zone_state: list):
+ if len(air_zone_state) != 6:
+ raise ValueError("air_input must have exactly 6 elements")
+ return [air_zone_state[4], air_zone_state[5], air_zone_state[2], air_zone_state[3], air_zone_state[0], air_zone_state[1]]
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage