From 5d488d56434f486b42dae04d5326a5a81106ad31 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Fri, 24 Jan 2025 02:31:52 -0800 Subject: initial commit --- .gitignore | 175 +++++++++++++ README.md | 14 ++ chuni-hands-evolved.py | 649 +++++++++++++++++++++++++++++++++++++++++++++++++ chuniio.py | 35 +++ 4 files changed, 873 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 chuni-hands-evolved.py create mode 100644 chuniio.py 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("", lambda e: on_mousewheel(e, x_spinbox)) + y_spinbox.bind("", lambda e: on_mousewheel(e, y_spinbox)) + spacing_spinbox.bind("", lambda e: on_mousewheel(e, spacing_spinbox)) + width_spinbox.bind("", 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("", 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]] -- cgit v1.2.3