From e1e3aa86f34d5c05be8a2e20a1c0b4639cf6f919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Paku=C5=82a?= Date: Tue, 1 Oct 2024 20:41:57 +0200 Subject: [PATCH] Sidequest - rewrite part 1 (#30) --- .gitignore | 4 + .vscode/launch.json | 33 ++ boxflat/app.py | 49 +-- boxflat/connection_manager.py | 493 +++++++--------------- boxflat/hid_handler.py | 165 ++++---- boxflat/moza_command.py | 92 +++- boxflat/panels/base.py | 164 +++---- boxflat/panels/h_pattern.py | 20 +- boxflat/panels/handbrake.py | 58 ++- boxflat/panels/home.py | 39 +- boxflat/panels/others.py | 84 ++-- boxflat/panels/pedals.py | 100 +++-- boxflat/panels/presets.py | 45 +- boxflat/panels/sequential.py | 29 +- boxflat/panels/settings_panel.py | 85 ++-- boxflat/panels/wheel.py | 235 ++++++----- boxflat/preset_handler.py | 20 +- boxflat/subscription.py | 245 +++++++++++ boxflat/widgets/__init__.py | 2 + boxflat/widgets/advance_row.py | 20 + boxflat/widgets/button_level_row.py | 4 +- boxflat/widgets/button_row.py | 3 +- boxflat/widgets/calibration_row.py | 80 ++-- boxflat/widgets/color_picker_row.py | 4 +- boxflat/widgets/combo_row.py | 2 +- boxflat/widgets/dialog_row.py | 15 +- boxflat/widgets/equalizer_row.py | 64 ++- boxflat/widgets/label_row.py | 6 +- boxflat/widgets/level_row.py | 19 +- boxflat/widgets/new_color_picker_row.py | 50 ++- boxflat/widgets/preferences_group.py | 19 +- boxflat/widgets/preset_dialog.py | 128 ++++++ boxflat/widgets/row.py | 75 ++-- boxflat/widgets/slider_row.py | 17 +- boxflat/widgets/switch_row.py | 10 +- boxflat/widgets/toggle_button_row.py | 6 +- data/serial.yml | 29 +- data/style.css | 9 +- data/version | 2 +- entrypoint.py | 3 +- io.github.lawstorant.boxflat.metainfo.xml | 20 +- 41 files changed, 1415 insertions(+), 1132 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 boxflat/subscription.py create mode 100644 boxflat/widgets/advance_row.py create mode 100644 boxflat/widgets/preset_dialog.py diff --git a/.gitignore b/.gitignore index 3b58779..28e4519 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,7 @@ cython_debug/ /.flatpak-builder /app + +.config +.vscode/* +!.vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..71d13db --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Normal", + "type": "debugpy", + "request": "launch", + "program": "entrypoint.py", + "console": "integratedTerminal", + "args": [ + "--local", + "--custom" + ] + }, + { + "name": "GTK Debug", + "type": "debugpy", + "request": "launch", + "program": "entrypoint.py", + "console": "internalConsole", + "env": { + "GTK_DEBUG": "interactive" + }, + "args": [ + "--local", + "--custom" + ] + } + ] +} diff --git a/boxflat/app.py b/boxflat/app.py index 9282a0a..0adc548 100644 --- a/boxflat/app.py +++ b/boxflat/app.py @@ -8,14 +8,15 @@ import os class MainWindow(Adw.ApplicationWindow): - def __init__(self, data_path: str, config_path: str, dry_run: bool, *args, **kwargs): + def __init__(self, data_path: str, config_path: str, dry_run: bool, custom: bool, *args, **kwargs): super().__init__(*args, **kwargs) self._hid_handler = HidHandler() self._config_path = config_path - self._cm = MozaConnectionManager(os.path.join(data_path, "serial.yml"), self._hid_handler ,dry_run) - # self.connect('close-request', lambda w: self._cm.shutdown()) + self._cm = MozaConnectionManager(os.path.join(data_path, "serial.yml"), dry_run) + self.connect('close-request', self._cm.shutdown) + self._cm.subscribe("hid-device-connected", self._hid_handler.add_device) with open(os.path.join(data_path, "version"), "r") as version: self._version = version.readline().strip() @@ -55,6 +56,8 @@ def __init__(self, data_path: str, config_path: str, dry_run: bool, *args, **kwa self.navigation = navigation self._prepare_settings() + if custom: + self._panels["Other"].enable_custom_commands() buttons = self._panel_buttons() for button in buttons: @@ -81,26 +84,24 @@ def __init__(self, data_path: str, config_path: str, dry_run: bool, *args, **kwa self._alert.set_body_use_markup(True) self._alert.connect("response", self._handle_udev_dialog) - self._cm.subscribe_no_access(self.show_udev_dialog) + self._cm.subscribe("no-accesss", self.show_udev_dialog) - def switch_panel(self, button) -> None: + def switch_panel(self, button): new_title = button.get_child().get_label() new_content = self._panels[new_title].content if self.navigation.get_content() == new_content: return - self._cm.reset_subscriptions() self.navigation.set_content(new_content) - self._panels[new_title].activate_subs() - def set_content_title(self, title: str) -> None: + def set_content_title(self, title: str): self.navigation.get_content().set_title(title) - def show_udev_dialog(self) -> None: + def show_udev_dialog(self): self._alert.choose() @@ -109,13 +110,13 @@ def _handle_udev_dialog(self, dialog, response): self.open_url("https://github.com/Lawstorant/boxflat?tab=readme-ov-file#udev-rule-installation-for-flatpak") - def open_url(self, url: str) -> None: + def open_url(self, url: str): launcher = Gtk.UriLauncher() launcher.set_uri(url) launcher.launch() - def _prepare_settings(self) -> None: + def _prepare_settings(self): self._panels["Home"] = HomeSettings(self.switch_panel, self._dry_run, self._cm, self._hid_handler, self._version) self._panels["Base"] = BaseSettings(self.switch_panel, self._cm, self._hid_handler) self._panels["Wheel"] = WheelSettings(self.switch_panel, self._cm, self._hid_handler) @@ -123,12 +124,10 @@ def _prepare_settings(self) -> None: self._panels["H-Pattern Shifter"] = HPatternSettings(self.switch_panel, self._cm) self._panels["Sequential Shifter"] = SequentialSettings(self.switch_panel, self._cm) self._panels["Handbrake"] = HandbrakeSettings(self.switch_panel, self._cm, self._hid_handler) - self._panels["Other"] = OtherSettings(self.switch_panel, self._cm, self._hid_handler) - self._panels["Presets"] = PresetSettings(self.switch_panel, self._cm, self._config_path) + self._panels["Other"] = OtherSettings(self.switch_panel, self._cm, self._hid_handler, self._version) + self._panels["Presets"] = PresetSettings(self.switch_panel, self._cm, self._config_path, self._version) - self._panels["Other"].subscribe_brake_calibration( - self._panels["Pedals"].set_brake_calibration_active - ) + self._panels["Other"].subscribe("brake-calibration-active", self._panels["Pedals"].set_brake_calibration_active) for panel in self._panels.values(): panel.active(-2) @@ -138,22 +137,15 @@ def _prepare_settings(self) -> None: self._panels["Other"].active(1) self._panels["Presets"].active(1) - for panel in self._panels.values(): - panel.activate_subs_connected() - panel.activate_hid_subs() - if self._dry_run: print("Dry run") return - self._panels["Base"].activate_subs() - self._cm.set_rw_active(True) - self._hid_handler.start() + self._cm.set_write_active() def _activate_default(self) -> SettingsPanel: self._panels["Home"].button.set_active(True) - self._panels["Home"].activate_subs() return self._panels["Home"] @@ -166,17 +158,16 @@ def _panel_buttons(self) -> list: class MyApp(Adw.Application): - def __init__(self, data_path: str, config_path: str, dry_run: bool, **kwargs): + def __init__(self, data_path: str, config_path: str, dry_run: bool, custom: bool, **kwargs): super().__init__(**kwargs) self.connect('activate', self.on_activate) - self._data_path = data_path - self._dry_run = dry_run - self._config_path = config_path css_provider = Gtk.CssProvider() css_provider.load_from_path(f"{data_path}/style.css") Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + self.win = MainWindow(data_path, config_path, dry_run, custom) + def on_activate(self, app): - self.win = MainWindow(self._data_path, self._config_path, self._dry_run, application=app) + self.win.set_application(app) self.win.present() diff --git a/boxflat/connection_manager.py b/boxflat/connection_manager.py index 723b999..82f86c3 100644 --- a/boxflat/connection_manager.py +++ b/boxflat/connection_manager.py @@ -3,12 +3,11 @@ from binascii import hexlify from .moza_command import * from serial import Serial -from threading import Thread -from threading import Lock -from threading import Event -import struct +from threading import Thread, Lock, Event import time -from .hid_handler import HidHandler, MozaHidDevice +from .hid_handler import MozaHidDevice +from .subscription import SubscriptionList, EventDispatcher +from queue import SimpleQueue import gi gi.require_version('Gtk', '4.0') @@ -17,97 +16,88 @@ CM_RETRY_COUNT=2 HidDeviceMapping = { - "base" : MozaHidDevice.BASE, - "handbrake" : MozaHidDevice.HANDBRAKE, - "hpattern" : MozaHidDevice.HPATTERN, + "base" : MozaHidDevice.BASE, + "handbrake" : MozaHidDevice.HANDBRAKE, + "hpattern" : MozaHidDevice.HPATTERN, "sequential" : MozaHidDevice.SEQUENTIAL, - "pedals" : MozaHidDevice.PEDALS, - "hub" : MozaHidDevice.HUB, - "estop" : MozaHidDevice.ESTOP, - "main" : None + "pedals" : MozaHidDevice.PEDALS, + "hub" : MozaHidDevice.HUB, + "estop" : MozaHidDevice.ESTOP, + "main" : None } -class MozaConnectionManager(): - def __init__(self, serial_data_path: str, hid_handler: HidHandler, dry_run=False): + +class MozaQueueElement(): + def __init__(self, value=None, command_name=None): + self.value = value + self.command_name = command_name + + + +class MozaConnectionManager(EventDispatcher): + def __init__(self, serial_data_path: str, dry_run=False): + super().__init__() + self._serial_data = None self._dry_run = dry_run - self._shutdown = False - - self._hid_handler = hid_handler + self._shutdown = Event() self._serial_devices = {} self._devices_lock = Lock() - self._command_lock = Lock() with open(serial_data_path) as stream: try: self._serial_data = yaml.safe_load(stream) except yaml.YAMLError as exc: print(exc) - self._shutdown = True + self._shutdown.set() quit(1) - self._serial_lock = Lock() + self._device_ids = self._serial_data["device-ids"] - self._refresh = Event() - self._refresh_cont = Event() - self._subscribtions = {} - self._refresh_thread = Thread(daemon=True, target=self._notify) - self._refresh_thread.start() + # register events + self._command_list = list(self._serial_data["commands"].keys()) + self._polling_list = [] + for command, data in self._serial_data["commands"].items(): + if self._device_ids[command.split("-")[0]] == -1: + continue + + if data["read"] == -1: + continue - self._connected_subscribtions = {} - self._connected_thread = Thread(daemon=True, target=self._notify_connected) - self._connected_thread.start() + self._polling_list.append(command) - self._cont_active = Event() - self._cont_subscribtions = {} - self._cont_thread = Thread(daemon=True, target=self._notify_cont) - # self._cont_thread.start() + self._register_events(*self._polling_list) + self._register_events("device-connected", "hid-device-connected") + self._register_events("shutdown", "no-access") - self._shutown_subscribtions = [] - self._no_access_subs = [] - self._sub_lock = Lock() + self._serial_lock = Lock() + self._refresh_cont = Event() + + self._connected_subscriptions = {} + self._connected_thread = None self._connected_lock = Lock() - self._cont_lock = Lock() - self._write_command_buffer = {} - self._read_command_buffer = {} - self._write_mutex = Lock() - self._read_mutex = Lock() - self._rw_thread = Thread(daemon=True, target=self._rw_handler) + self._write_queue = SimpleQueue() + self._write_thread = None self._message_start= int(self._serial_data["message-start"]) self._magic_value = int(self._serial_data["magic-value"]) self._serial_path = "/dev/serial/by-id" - self._set_type_method_mapping = { - "int" : self.set_setting_int, - "float" : self.set_setting_float, - "array" : self.set_setting_list, - "hex" : self.set_setting_hex - } - - self._get_type_method_mapping = { - "int" : self.get_setting_int, - "float" : self.get_setting_float, - "array" : self.get_setting_list, - "hex" : self.get_setting_hex - } - - def shutdown(self) -> None: - self._shutdown = True - for sub in self._shutown_subscribtions: - sub[0](*sub[1]) + def shutdown(self, *rest): + self._dispatch("shutdown") + self._shutdown.set() - def device_discovery(self, *args) -> None: + def device_discovery(self, *args): # print("\nDevice discovery...") path = self._serial_path if not os.path.exists(path): - print("No devices found!") + # print("No devices found!") self._handle_devices({}) return @@ -140,200 +130,111 @@ def device_discovery(self, *args) -> None: # print("Pedals found") # TODO: Check this info somehow - elif device.lower().find("hub") != -1: - serial_devices["hub"] = device + # elif device.lower().find("hub") != -1: + # serial_devices["hub"] = device # print("Hub found") - elif device.lower().find("stop") != -1: - serial_devices["estop"] = device + # elif device.lower().find("stop") != -1: + # serial_devices["estop"] = device # print("E-Stop found") self._handle_devices(serial_devices) - # print("Device discovery end\n") - def _handle_devices(self, new_devices: dict) -> None: + def _handle_devices(self, new_devices: dict): old_devices = None - with self._devices_lock: old_devices = self._serial_devices self._serial_devices = new_devices for device in new_devices: if device not in old_devices: - self._hid_handler.add_device(HidDeviceMapping[device]) - - - - def subscribe(self, command: str, callback: callable, *args) -> None: - if not command in self._subscribtions: - self._subscribtions[command] = [] - - self._subscribtions[command].append((callback, args)) - - - def subscribe_connected(self, command: str, callback: callable, *args) -> None: - if not command in self._connected_subscribtions: - self._connected_subscribtions[command] = [] - - self._connected_subscribtions[command].append((callback, args)) + self._dispatch("device-connected", device) + self._dispatch("hid-device-connected", HidDeviceMapping[device]) + if len(new_devices) == 0 and self._refresh_cont.is_set(): + self.refresh_cont(False) - def reset_subscriptions(self) -> None: - # print("\nClearing subscriptions") - with self._sub_lock: - self._subscribtions.clear() + elif len(new_devices) > 0 and not self._refresh_cont.is_set(): + self.refresh_cont(True) - with self._cont_lock: - self._cont_subscribtions.clear() + def subscribe_connected(self, command: str, callback: callable, *args): + if not command in self._connected_subscriptions: + self._connected_subscriptions[command] = SubscriptionList() + self._connected_subscriptions[command].append(callback, *args) - def subscribe_cont(self, command: str, callback: callable, *args) -> None: - if not command in self._cont_subscribtions: - self._cont_subscribtions[command] = [] - self._cont_subscribtions[command].append((callback, args)) - - - def subscribe_shutdown(self, callback, *args) -> None: - self._shutown_subscribtions.append((callback, args)) - - - def subscribe_no_access(self, callback, *args) -> None: - self._no_access_subs.append((callback, args)) - - - def refresh(self, *args) -> None: - self._refresh.set() - - - def refresh_cont(self, active: bool) -> None: + def refresh_cont(self, active: bool): if active: self._refresh_cont.set() - self._refresh.set() + Thread(daemon=True, target=self._polling_thread).start() else: self._refresh_cont.clear() - self._refresh.clear() - - - def _notify(self) -> None: - response = 0 - while not self._shutdown: - if not self._refresh.wait(2): - continue - if not self._refresh_cont.is_set(): - self._refresh.clear() - with self._sub_lock: - subs = dict(self._subscribtions) + def _polling_thread(self): + while self._refresh_cont.is_set(): + time.sleep(0.2) + device = "laptop" # lol, so random! + for command in self._polling_list: + if command.startswith(device): + continue - for com in subs.keys(): - com_type = self._serial_data["commands"][com]["type"] - if com_type == "array": - response = self.get_setting_list(com) - elif com_type == "float": - response = self.get_setting_float(com) - elif com_type == "hex": - response = self.get_setting_hex(com) - else: - response = self.get_setting_int(com) + if self._event_sub_count(command) == 0: + continue + # print("Polling data: " + command) + response = self.get_setting(command) if response == -1: + device = command.split("-")[0] continue - for subscriber in subs[com]: - subscriber[0](response, *subscriber[1]) + self._dispatch(command, response) - if self._refresh_cont.is_set(): - time.sleep(1) - - - def _notify_connected(self) -> None: - response = 0 - while not self._shutdown: - if not self._refresh.wait(2): - continue + def _device_polling(self): + while not self._shutdown.is_set(): self.device_discovery() - with self._sub_lock: - subs = dict(self._connected_subscribtions) - - for com in subs.keys(): - com_type = self._serial_data["commands"][com]["type"] - if com_type == "array": - response = self.get_setting_list(com) - elif com_type == "float": - response = self.get_setting_float(com) - elif com_type == "hex": - response = self.get_setting_hex(com) - else: - response = self.get_setting_int(com) - - self._no_access_subs = [] - - for subscriber in subs[com]: - subscriber[0](response, *subscriber[1]) + with self._connected_lock: + lists = self._connected_subscriptions.copy() - if self._refresh_cont.is_set(): - time.sleep(1) + self._clear_event_subscriptions("no-access") + for command, subs in lists.items(): + subs.call_with_value(self.get_setting(command)) + time.sleep(1) + self._connected_thread = None - def _notify_cont(self) -> None: - while not self._shutdown: - if not self._cont_active.wait(2): - continue - - time.sleep(1/40) # 40 Hz refresh rate - with self._cont_lock: - subs = dict(self._cont_subscribtions) - - for com in subs.keys(): - response = self.get_setting_int(com) - if response == -1: - break - for subscriber in subs[com]: - GLib.idle_add(subscriber[0], response, *subscriber[1]) - + def _notify_no_access(self): + self._dispatch("no-access") + self._clear_event_subscriptions("no-access") - def _notify_no_access(self) -> None: - for i in range(len(self._no_access_subs)): - subscriber = self._no_access_subs[i] - GLib.idle_add(subscriber[0], *subscriber[1]) - self._no_access_subs.pop(i) - - def set_cont_active(self, active: bool) -> None: + def set_cont_active(self, active: bool): if active: self._cont_active.set() else: self._cont_active.clear() - def set_rw_active(self, *args) -> None: - if not self._rw_thread.is_alive(): - self._rw_thread.start() - - - def _rw_handler(self) -> None: - while not self._shutdown: - time.sleep(0.1) - - with self._write_mutex: - write_buffer = self._write_command_buffer - self._write_command_buffer = {} + def set_write_active(self, *args): + if not self._write_thread: + self._write_thread = Thread(daemon=True, target=self._write_handler) + self._write_thread.start() - for com in write_buffer.keys(): - self._handle_command(com, MOZA_COMMAND_WRITE, write_buffer[com][0], write_buffer[com][1]) + if not self._connected_thread: + self._connected_thread = Thread(daemon=True, target=self._device_polling) + self._connected_thread.start() - def _calculate_checksum(self, data: bytes) -> int: - value = self._magic_value - for d in data: - value += int(d) - return value % 256 + def _write_handler(self): + while not self._shutdown.is_set(): + element = self._write_queue.get() + self.handle_setting(element.value, element.command_name, True) + self._write_thread = None def _get_device_id(self, device_type: str) -> int: @@ -357,42 +258,42 @@ def _get_device_path(self, device_type: str) -> str: def send_serial_message(self, serial_path: str, message: bytes, read_response=False) -> bytes: - msg = "" - for b in message: - msg += f"{hex(b)} " + # msg = "" + # for b in message: + # msg += f"{hex(b)} " # print(f"\nDevice: {serial_path}") # print(f"Sending: {msg}") if self._dry_run: - return bytes(1) + return if serial_path == None: # print("No compatible device found!") - return None + return initial_len = message[1] rest = bytes() length = 0 cmp = bytes([self._message_start]) - start = bytes(1) + start = bytes() self._serial_lock.acquire() try: - serial = Serial(serial_path, baudrate=115200, timeout=0.05) - # time.sleep(1/500) + serial = Serial(serial_path, baudrate=115200, timeout=0.01) + time.sleep(1/500) serial.reset_output_buffer() serial.reset_input_buffer() for i in range(CM_RETRY_COUNT): serial.write(message) - time.sleep(1/500) + #time.sleep(1/500) # read_response = True # For teesting writes start_time = time.time() while read_response: - if time.time() - start_time > 0.04: + if time.time() - start_time > 0.01: + # print("Time's up!") read_response = False - message = None break start = serial.read(1) @@ -411,160 +312,89 @@ def send_serial_message(self, serial_path: str, message: bytes, read_response=Fa break serial.close() + + #print(time.time() - start_time) except Exception as error: - print("Error opening device!") + # print("Error opening device!") read_response = False self._notify_no_access() self._serial_lock.release() if read_response == False: - return message + return message = bytearray() message.extend(cmp) message.append(length) message.extend(rest) - msg = "" - for b in message: - msg += f"{hex(b)} " + # msg = "" + # for b in message: + # msg += f"{hex(b)} " # print(f"Response: {msg}") - return message - - - # Handle command operations - def _handle_command(self, command_name: str, rw, value: int=1, byte_value: bytes=None) -> bytes: - if not command_name in self._serial_data["commands"]: - return bytes(1) - command = MozaCommand(command_name, self._serial_data["commands"]) - - if command.length == -1 or command.id == -1: - print("Command undiscovered") - return bytes(1) - - if rw == MOZA_COMMAND_READ and command.read_group == -1: - print("Command doesn't support READ access") - return bytes(1) - - if rw == MOZA_COMMAND_WRITE and command.write_group == -1: - print("Command doesn't support WRITE access") - return None - - if byte_value != None: - command.set_payload_bytes(byte_value) - else: - command.payload = value - - device_id = self._get_device_id(command.device_type) - if device_id == -1: - print("Device ID undiscovered yet") - return bytes(1) - - device_path = self._get_device_path(command.device_type) - message = command.prepare_message(self._message_start, device_id, rw, self._calculate_checksum) - - # WE get a response without the checksum - read = (rw == MOZA_COMMAND_READ) - initial_len = command.payload_length - response = self.send_serial_message(device_path, message, read) - - if response == None: - return None - - # if len(response) != len(message): - # return None + return bytes(message) - # check if length is 2 or lower because we need the - # device id in the response, not just the value - # length = response[1] - # if length <= command.length+1: - # return bytes(1) - length = command.payload_length - return response[-1-length:-1] + def _handle_command_v2(self, command_data: MozaCommand, rw: int) -> bytes: + message = command_data.prepare_message(self._message_start, rw, self._magic_value) + device_path = self._get_device_path(command_data.device_type) - # Set a setting value on a device - # If value should be float, provide bytes - def _set_setting(self, command_name: str, value: int=0, byte_value=None) -> None: - with self._write_mutex: - # TODO: use Queue here instead of my custom implementation - self._write_command_buffer[command_name] = (value, byte_value) + response = self.send_serial_message(device_path, message, (rw == MOZA_COMMAND_READ)) + if not response is None: + response = response[-1-command_data.payload_length:-1] + # only return payload + return response - def set_setting_int(self, value: int, command_name: str) -> None: - self._set_setting(command_name, value) + def handle_setting(self, value, command_name: str, rw: int) -> bool: + if command_name not in self._command_list: + print("Command not found: " + command_name) + return - def set_setting_float(self, value: float, command_name: str) -> None: - self._set_setting(command_name, byte_value=struct.pack(">f", float(value))) - - - def set_setting_list(self, values: list, command_name: str) -> None: - self._set_setting(command_name, byte_value=bytes(values)) - - - def set_setting_hex(self, value: str, command_name: str) -> None: - self._set_setting(command_name, byte_value=bytes.fromhex(value)) - - - def set_setting_auto(self, value, command_name: str) -> bool: - if command_name not in self._serial_data["commands"]: - return False - - value_type = self._serial_data["commands"][command_name]["type"] - self._set_type_method_mapping[value_type](value, command_name) - return True + command = MozaCommand(command_name, self._serial_data["commands"]) + command.device_id = self._get_device_id(command.device_type) + if command.device_id == -1: + print("Invalid Device ID") + return - # Get a setting value from a device - def _get_setting(self, command_name: str) -> bytes: - return self._handle_command(command_name, MOZA_COMMAND_READ) + if rw == MOZA_COMMAND_WRITE and command.write_group == -1: + print("Command doesn't support WRITE operation: " + command_name) + return + elif rw == MOZA_COMMAND_READ and command.read_group == -1: + print("Command doesn't support READ operation: " + command_name) + return - def get_setting_int(self, command_name: str) -> int: - response = self._get_setting(command_name) + command.set_payload(value) + response = self._handle_command_v2(command, rw) if response == None: - return -1 - return int.from_bytes(response) - + return - def get_setting_list(self, command_name: str) -> list: - response = self._get_setting(command_name) - if response == None: - return -1 - return list(response) + command.set_payload_bytes(response) + return command.get_payload() - def get_setting_float(self, command_name: str) -> float: - response = self._get_setting(command_name) - if response == None: - return -1 - return struct.unpack(">f", response)[0] + def set_setting(self, value, command_name: str): + self._write_queue.put(MozaQueueElement(value, command_name)) - def get_setting_hex(self, command_name: str) -> str: - response = self._get_setting(command_name) + def get_setting(self, command_name: str): + response = self.handle_setting(1, command_name, MOZA_COMMAND_READ) if response == None: return -1 - return hexlify(response).decode("utf-8") - - - def get_setting_auto(self, command_name: str): - if command_name not in self._serial_data["commands"]: - return - - value_type = self._serial_data["commands"][command_name]["type"] - return self._get_type_method_mapping[value_type](command_name) + return response def cycle_wheel_id(self) -> int: with self._devices_lock: wid = self._serial_data["device-ids"]["wheel"] - 1 - if wid == self._serial_data["device-ids"]["base"]: - wid = self._serial_data["device-ids"]["pedals"] - 1 + if wid == self._serial_data["device-ids"]["base"] + 1: + wid = self._serial_data["device-ids"]["pedals"] - 2 self._serial_data["device-ids"]["wheel"] = wid @@ -576,7 +406,4 @@ def get_command_data(self) -> dict: return self._serial_data["commands"] -# TODO: Move value conversion to MozaCommand -# TODO: Get rid of helper methods for setting/getting settings. -# TODO: Simplify command handler # TODO: Rewrite manager so it keeps a read and write connection open constantly. diff --git a/boxflat/hid_handler.py b/boxflat/hid_handler.py index 3a517f0..27aa6aa 100644 --- a/boxflat/hid_handler.py +++ b/boxflat/hid_handler.py @@ -3,6 +3,7 @@ from time import sleep from evdev.ecodes import * +from .subscription import EventDispatcher, Observable from threading import Thread, Lock, Event @@ -24,13 +25,9 @@ class MozaHidDevice(): class AxisData(): - name: str - device: str - base_offset: int - - def __init__(self, n, d): - self.name = n - self.device = d + def __init__(self, name: str, device: str): + self.name = name + self.device = device class MozaAxis(): @@ -46,7 +43,21 @@ class MozaAxis(): HANDBRAKE = AxisData("handbrake", MozaHidDevice.HANDBRAKE) -MozaAxisCodes = { +MOZA_AXIS_LIST = [ + "steering", + "throttle", + "brake", + "clutch", + "combined", + "left_paddle", + "right_paddle", + "stick_x", + "stick_y", + "handbrake" +] + + +MOZA_AXIS_CODES = { "ABS_RX" : MozaAxis.THROTTLE.name, "ABS_RY" : MozaAxis.BRAKE.name, "ABS_RZ" : MozaAxis.CLUTCH.name, @@ -54,7 +65,7 @@ class MozaAxis(): } -MozaAxisBaseCodes = { +MOZA_AXIS_BASE_CODES = { "ABS_X" : MozaAxis.STEERING.name, "ABS_Z" : MozaAxis.THROTTLE.name, "ABS_RZ" : MozaAxis.BRAKE.name, @@ -68,28 +79,66 @@ class MozaAxis(): } -class HidHandler(): +MOZA_BUTTON_COUNT = 128 + + +class AxisValue(): + def __init__(self, name: str): + self.name = name + self._lock = Lock() + self._value = 0 + + + @property + def value(self) -> int: + with self._lock: + return self._value + + + @value.setter + def value(self, new_value: int): + with self._lock: + self._value = new_value + + + @property + def data(self) -> tuple[str, int]: + return self.name, self.value + + +class HidHandler(EventDispatcher): def __init__(self): - self._axis_subs = {} + super().__init__() + self._axis_values = {} - self._button_subs = {} + for i in range(0, MOZA_BUTTON_COUNT): + self._register_event(f"button-{i}") - self._shutdown = False - self._update_rate = 120 + for name in MOZA_AXIS_LIST: + self._axis_values[name] = AxisValue(name) + self._register_event(name) - self._device_patterns = [] - self._devices = [] + self._running = Event() + self._update_rate = 120 self._base = None - - self._axis_values_lock = Lock() + self._device_count = Observable(0) + self._device_count.subscribe(self._device_count_changed) def __del__(self): - self.shutdown() + self.stop() - def start(self): - Thread(target=self._notify_axis, daemon=True).start() + def _device_count_changed(self, new_count): + if new_count == 0: + self.stop() + + elif not self._running.is_set(): + Thread(target=self._axis_data_polling, daemon=True).start() + + + def stop(self): + self._running.clear() def get_update_rate(self) -> int: @@ -104,17 +153,16 @@ def set_update_rate(self, rate: int) -> bool: return True - def add_device(self, pattern: MozaHidDevice) -> None: + def add_device(self, pattern: MozaHidDevice): if not pattern: return - devices = [evdev.InputDevice(path) for path in evdev.list_devices()] - device = None + devices = [evdev.InputDevice(path) for path in evdev.list_devices()] for hid in devices: if re.search(pattern, hid.name.lower()): - print(f"HID device \"{hid.name}\" found") + print(f"HID device found: " + hid.name) device = hid if device != None: @@ -142,40 +190,19 @@ def add_device(self, pattern: MozaHidDevice) -> None: if device.absinfo(ecode).fuzz > 8: device.set_absinfo(ecode, fuzz=fuzz) - thread = Thread(daemon=True, target=self._read_loop, args=[device]) - thread.start() - - - def subscribe_axis(self, axis: AxisData, callback: callable, *args) -> None: - if not axis.name in self._axis_subs: - self._axis_subs[axis.name] = [] - self._axis_values[axis.name] = 0 - - self._axis_subs[axis.name].append((callback, args)) + Thread(daemon=True, target=self._hid_read_loop, args=[device]).start() + self._device_count.value += 1 - def subscribe_button(self, number, callback: callable, *args) -> None: - # if not button in self._button_subs: - # self._button_subs[number] = [] - - # self._button_subs[number].append((callback, args)) - pass - - - def _notify_axis(self) -> None: - axis_values = {} - while not self._shutdown: + def _axis_data_polling(self): + self._running.set() + while self._running.is_set(): sleep(1/self._update_rate) + for axis in self._axis_values.values(): + self._dispatch(*axis.data) - with self._axis_values_lock: - axis_values = self._axis_values.copy() - - for axis, value in self._axis_values.items(): - for sub in self._axis_subs[axis]: - sub[0](value, *sub[1]) - - def _update_axis(self, device: evdev.InputDevice, code: int, value: int) -> None: + def _update_axis(self, device: evdev.InputDevice, code: int, value: int): axis_min = device.absinfo(code).min code = evdev.ecodes.ABS[code] name = "" @@ -184,45 +211,39 @@ def _update_axis(self, device: evdev.InputDevice, code: int, value: int) -> None value += abs(axis_min) if device == self._base: - name = MozaAxisBaseCodes[code] + name = MOZA_AXIS_BASE_CODES[code] else: - name = MozaAxisCodes[code] + name = MOZA_AXIS_CODES[code] # print(f"axis {name} ({code}), value: {value}, min: {axis_min}") - - if name in self._axis_values: - with self._axis_values_lock: - self._axis_values[name] = value + self._axis_values[name].value = value - def _update_button(self, number: int, state: int) -> None: + def _notify_button(self, number: int, state: int): if number <= BTN_DEAD: number -= BTN_JOYSTICK - 1 else: - number -= KEY_NEXT_FAVORITE - (BTN_DEAD - BTN_JOYSTICK) -2 + number -= KEY_NEXT_FAVORITE - (BTN_DEAD - BTN_JOYSTICK) - 2 - print(f"button {number}, state: {state}") + #print(f"button {number}, state: {state}") + self._dispatch("button-" + str(number), state) - # if number in self._button_subs.keys(): - # for sub in self._button_subs[number]: - # sub[0](number, state*sub[1]) - - def _read_loop(self, device: evdev.InputDevice) -> None: - sleep(1) + def _hid_read_loop(self, device: evdev.InputDevice): + sleep(0.5) try: for event in device.read_loop(): if event.type == EV_ABS: self._update_axis(device, event.code, event.value) # elif event.type == EV_KEY: - # self._update_button(event.code, event.value) + # self._notify_button(event.code, event.value) except Exception as e: # print(e) pass - print(f"HID device \"{device.name}\" disconnected") + print(f"HID device disconnected: " + device.name) + self._device_count.value -= 1 if device == self._base: self._base = None - device = None diff --git a/boxflat/moza_command.py b/boxflat/moza_command.py index 79b44f8..20daccd 100644 --- a/boxflat/moza_command.py +++ b/boxflat/moza_command.py @@ -1,26 +1,33 @@ from sys import byteorder +from binascii import hexlify +from struct import pack, unpack MOZA_COMMAND_READ=0 MOZA_COMMAND_WRITE=1 +MOZA_COMMAND_DEAD=2 class MozaCommand(): def __init__(self, name:str, commands_data: object): self.id = list(commands_data[name]["id"]) self.read_group = int(commands_data[name]["read"]) self.write_group = int(commands_data[name]["write"]) + self._length = int(commands_data[name]["bytes"]) self._payload = bytes(self.length) + self.name = name.split("-", maxsplit=1)[1] self._device_type = name.split("-")[0] self._type = commands_data[name]["type"] + self._device_id = None + @property def payload(self) -> bytes: - return self._payload + return self.get_payload_bytes() @payload.setter - def payload(self, value: int) -> None: - self._payload = value.to_bytes(self._length) + def payload(self, value): + self.set_payload(value) @property def id_bytes(self) -> bytes: @@ -54,12 +61,79 @@ def device_type(self) -> str: def type(self) -> str: return self._type - def set_payload_bytes(self, value: bytes) -> None: + + @property + def device_id(self) -> int: + return self._device_id + + + @device_id.setter + def device_id(self, new_id: int): + if isinstance(new_id, int): + self._device_id = new_id + + + def set_payload_bytes(self, value: bytes): self._payload = value + + def get_payload_bytes(self) -> bytes: + return self._payload + + + def set_payload(self, value): + data = None + try: + if self._type == "int": + data = int(value).to_bytes(self._length) + + elif self._type == "float": + data = pack(">f", float(value)) + + elif self._type == "array": + if isinstance(value, list): + data = bytes(value) + else: + data = bytes(self._length) + + elif self._type == "hex": + data = bytes.fromhex(value) + except: + data = bytes(self._length) + + self._payload = data + + + def get_payload(self): + data = self._payload + if self._type == "int": + data = int.from_bytes(data) + + elif self._type == "float": + data = unpack(">f", data)[0] + + elif self._type == "array": + data = list(data) + + elif self._type == "hex": + data = hexlify(data).decode("utf-8") + + return data + + + def get_payload_length(self) -> int: + return self._length + + + def _calculate_checksum(self, data: bytes, magic_value: int) -> int: + value = magic_value + for d in data: + value += int(d) + return value % 256 + + def prepare_message(self, start_value: int, - device_id: int, rw: int, - check_function: callable=None) -> bytes: + rw: int, magic_value: int) -> bytes: ret = bytearray() ret.append(start_value) @@ -70,11 +144,9 @@ def prepare_message(self, start_value: int, elif rw == MOZA_COMMAND_WRITE: ret.extend(self.write_group_byte) - ret.append(device_id) + ret.append(self._device_id) ret.extend(self.id_bytes) ret.extend(self._payload) - - if check_function != None: - ret.append(check_function(ret)) + ret.append(self._calculate_checksum(ret, magic_value)) return bytes(ret) diff --git a/boxflat/panels/base.py b/boxflat/panels/base.py index 8dd8c84..338d43a 100644 --- a/boxflat/panels/base.py +++ b/boxflat/panels/base.py @@ -7,7 +7,7 @@ import time class BaseSettings(SettingsPanel): - def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler) -> None: + def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler): self._curve_row = None self._eq_row = None @@ -39,96 +39,98 @@ def __init__(self, button_callback: callable, connection_manager: MozaConnection ] super().__init__("Base", button_callback, connection_manager, hid_handler) - self._append_sub_connected("base-limit", self.active) + self._cm.subscribe_connected("base-limit", self.active) - def _set_rotation(self, value: int) -> None: - self._cm.set_setting_int(value, "base-limit") - self._cm.set_setting_int(value, "base-max-angle") + def _set_rotation(self, value: int): + self._cm.set_setting(value, "base-limit") + self._cm.set_setting(value, "base-max-angle") - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_view_stack() self.add_preferences_page("Base") self.add_preferences_group("Important settings", alt_level_bar=True) self._current_group.set_bar_max(32767) self._current_group.set_offset(-32767) - self._append_sub_hid(MozaAxis.STEERING, self._current_group.set_alt_bar_level) + self._hid_handler.subscribe(MozaAxis.STEERING.name, self._current_group.set_alt_bar_level) self._add_row(BoxflatSliderRow( "Wheel Rotation Angle",subtitle="Round and round", range_start=90, range_end=2700, big=True, draw_value=False)) - self._current_row.add_marks(360, 540, 720, 900, 1080, 1440, 1800, 2160, 2520) + self._current_row.add_marks(360, 540, 720, 900, 1080, 1440, 1800, 2160) + self._current_row.add_mark(2520, "2520 ") self._current_row.set_expression("/2") self._current_row.set_reverse_expression("*2") self._current_row.subscribe(self._set_rotation) - self._append_sub("base-limit", self._current_row.set_value) + self._cm.subscribe("base-limit", self._current_row.set_value) self._add_row(BoxflatSliderRow("FFB Strength", suffix="%")) self._current_row.add_marks(25, 50, 75) self._current_row.set_expression("*10") self._current_row.set_reverse_expression("/10") - self._current_row.subscribe(self._cm.set_setting_int, "base-ffb-strength") - self._append_sub("base-ffb-strength", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-ffb-strength") + self._cm.subscribe("base-ffb-strength", self._current_row.set_value) self._add_row(BoxflatSwitchRow("Force Feedback Enabled")) self._current_row.reverse_values() - self._current_row.subscribe(self._cm.set_setting_int, "main-set-ffb-status") - self._append_sub("main-get-ffb-status", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "main-set-ffb-status") + self._cm.subscribe("main-get-ffb-status", self._current_row.set_value) self._add_row(BoxflatButtonRow("Adjust center point", "Center")) - self._current_row.subscribe(self._cm.set_setting_int, "base-calibration") + self._current_row.subscribe(self._cm.set_setting, "base-calibration") # Basic settings self.add_preferences_group("Basic settings") - self._add_row(BoxflatSliderRow("Road Sensitivity", range_end=10)) + self._sensitivity_row = BoxflatSliderRow("Road Sensitivity", range_end=10) + self._add_row(self._sensitivity_row) self._current_row.add_marks(2, 4, 6, 8) self._current_row.set_expression("*4 + 10") self._current_row.set_reverse_expression("/4 - 2.5") - self._current_row.subscribe(self._cm.set_setting_int, "base-road-sensitivity") - self._current_row.subscribe(self._set_eq_preset, raw=True) - self._append_sub("base-road-sensitivity", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-road-sensitivity") + self._current_row.subscribe(self._set_eq_preset, True) + self._cm.subscribe("base-road-sensitivity", self._current_row.set_value) self._add_row(BoxflatSliderRow("Maximum Wheel Speed", suffix="%", range_end=200)) self._current_row.add_marks(50, 100, 150) self._current_row.set_expression("*10") self._current_row.set_reverse_expression("/10") - self._current_row.subscribe(self._cm.set_setting_int, "base-speed") - self._append_sub("base-speed", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-speed") + self._cm.subscribe("base-speed", self._current_row.set_value) self._add_row(BoxflatSliderRow("Wheel Spring", suffix="%")) self._current_row.add_marks(50) self._current_row.set_expression("*10") self._current_row.set_reverse_expression("/10") - self._current_row.subscribe(self._cm.set_setting_int, "base-spring") - self._append_sub("base-spring", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-spring") + self._cm.subscribe("base-spring", self._current_row.set_value) self._add_row(BoxflatSliderRow("Wheel Damper", suffix="%")) self._current_row.add_marks(10, 25, 50) self._current_row.set_expression("*10") self._current_row.set_reverse_expression("/10") - self._current_row.subscribe(self._cm.set_setting_int, "base-damper") - self._append_sub("base-damper", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-damper") + self._cm.subscribe("base-damper", self._current_row.set_value) # Advenced settings self.add_preferences_group("Advenced Settings") self._add_row(BoxflatSliderRow("Torque output", suffix="%")) self._current_row.add_marks(25, 50, 75) - self._current_row.subscribe(self._cm.set_setting_int, "base-torque") - self._append_sub("base-torque", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-torque") + self._cm.subscribe("base-torque", self._current_row.set_value) self._add_row(BoxflatSliderRow("Natural Inertia", range_start=100, range_end=500, increment=50)) self._current_row.add_marks(150, 300) self._current_row.set_expression("*10") self._current_row.set_reverse_expression("/10") - self._current_row.subscribe(self._cm.set_setting_int, "base-inertia") - self._append_sub("base-inertia", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-inertia") + self._cm.subscribe("base-inertia", self._current_row.set_value) self._add_row(BoxflatSliderRow("Wheel Friction", suffix="%")) self._current_row.add_marks(10, 30) self._current_row.set_expression("*10") self._current_row.set_reverse_expression("/10") - self._current_row.subscribe(self._cm.set_setting_int, "base-friction") - self._append_sub("base-friction", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-friction") + self._cm.subscribe("base-friction", self._current_row.set_value) self.add_preferences_group("Protection") @@ -136,34 +138,37 @@ def prepare_ui(self) -> None: mode = BoxflatToggleButtonRow("Protection Mode") self._add_row(BoxflatSwitchRow("Hands-Off Protection")) - self._current_row.subscribe(self._cm.set_setting_int, "base-protection") + self._current_row.subscribe(self._cm.set_setting, "base-protection") self._current_row.subscribe(slider.set_active) self._current_row.subscribe(mode.set_active) - self._append_sub("base-protection", self._current_row.set_value) + self._cm.subscribe("base-protection", self._current_row.set_value) self._add_row(mode) self._current_row.add_buttons("Mode 1", "Mode 2") self._current_row.set_expression("+1") self._current_row.set_reverse_expression("-1") - self._current_row.subscribe(self._cm.set_setting_int, "base-protection-mode") - self._append_sub("base-protection-mode", self._current_row.set_value) - self._append_sub("base-protection", self._current_row.set_active) + self._current_row.subscribe(self._cm.set_setting, "base-protection-mode") + self._cm.subscribe("base-protection-mode", self._current_row.set_value) + self._cm.subscribe("base-protection", self._current_row.set_active) self._add_row(slider) - self._current_row.add_marks(900, 1550, 2800, 3500) - self._current_row.subscribe(self._cm.set_setting_int, "base-natural-inertia") - self._append_sub("base-natural-inertia", self._current_row.set_value) - self._append_sub("base-protection", self._current_row.set_active) + self._current_row.add_marks(2800) + self._current_row.add_mark(1100, "1100 ") + self._current_row.add_mark(1550, " 1550") + self._current_row.add_mark(3500, "3500 ") + self._current_row.subscribe(self._cm.set_setting, "base-natural-inertia") + self._cm.subscribe("base-natural-inertia", self._current_row.set_value) + self._cm.subscribe("base-protection", self._current_row.set_active) self._add_row(BoxflatSliderRow("Speed-depended Damping", suffix="%")) self._current_row.add_marks(50) - self._current_row.subscribe(self._cm.set_setting_int, "base-speed-damping") - self._append_sub("base-speed-damping", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-speed-damping") + self._cm.subscribe("base-speed-damping", self._current_row.set_value) self._add_row(BoxflatSliderRow("Speed-depended Damping", range_end=400, suffix=" kph ")) self._current_row.add_marks(120) - self._current_row.subscribe(self._cm.set_setting_int, "base-speed-damping-point") - self._append_sub("base-speed-damping-point", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-speed-damping-point") + self._cm.subscribe("base-speed-damping-point", self._current_row.set_value) # FFB Equalizer self.__prepare_eq() @@ -178,55 +183,55 @@ def prepare_ui(self) -> None: self._current_row.add_marks(4, 6, 8) self._current_row.set_expression("*(400/9)-(400/9)+100") self._current_row.set_reverse_expression("/(400/9) - 2.25 + 1") - self._current_row.subscribe(self._cm.set_setting_int, "base-soft-limit-stiffness") - self._append_sub("base-soft-limit-stiffness", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-soft-limit-stiffness") + self._cm.subscribe("base-soft-limit-stiffness", self._current_row.set_value) self._add_row(BoxflatToggleButtonRow("Soft Limit Strength")) self._current_row.add_buttons("Soft", "Middle", "Hard") self._current_row.set_expression("*22+56") self._current_row.set_reverse_expression("/22 - 2.5454") - self._current_row.subscribe(self._cm.set_setting_int, "base-soft-limit-strength") - self._append_sub("base-soft-limit-strength", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-soft-limit-strength") + self._cm.subscribe("base-soft-limit-strength", self._current_row.set_value) self._add_row(BoxflatSwitchRow("Soft Limit Retain Game FFB")) - self._current_row.subscribe(self._cm.set_setting_int, "base-soft-limit-retain") - self._append_sub("base-soft-limit-retain", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-soft-limit-retain") + self._cm.subscribe("base-soft-limit-retain", self._current_row.set_value) self.add_preferences_group("Misc") self._add_row(BoxflatSwitchRow("Base Status Indicator")) self._current_row.set_subtitle("Does nothing if your base doesn't have it") - self._current_row.subscribe(self._cm.set_setting_int, "main-set-led-status") - self._append_sub("main-get-led-status", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "main-set-led-status") + self._cm.subscribe("main-get-led-status", self._current_row.set_value) self._add_row(BoxflatSwitchRow("Default Force Feedback State")) self._current_row.reverse_values() - self._current_row.subscribe(self._cm.set_setting_int, "main-set-default-ffb-status") - self._append_sub("main-get-default-ffb-status", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "main-set-default-ffb-status") + self._cm.subscribe("main-get-default-ffb-status", self._current_row.set_value) self._add_row(BoxflatToggleButtonRow("Temperature Control Strategy",)) self._current_row.set_subtitle("Conservative = 50°C, Radical = 60°C") self._current_row.add_buttons("Conservative", "Radical") - self._current_row.subscribe(self._cm.set_setting_int, "base-temp-strategy") - self._append_sub("base-temp-strategy", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-temp-strategy") + self._cm.subscribe("base-temp-strategy", self._current_row.set_value) self.add_preferences_group("Temperatures") self._add_row(BoxflatLabelRow("MCU Temperature")) self._current_row.set_suffix("°C") self._current_row.set_reverse_expression("/100") - self._append_sub("base-mcu-temp", self._current_row.set_value) + self._cm.subscribe("base-mcu-temp", self._current_row.set_value) self._add_row(BoxflatLabelRow("MOSFET Temperature")) self._current_row.set_suffix("°C") self._current_row.set_reverse_expression("/100") - self._append_sub("base-mosfet-temp", self._current_row.set_value) + self._cm.subscribe("base-mosfet-temp", self._current_row.set_value) self._add_row(BoxflatLabelRow("Motor Temperature")) self._current_row.set_suffix("°C") self._current_row.set_reverse_expression("/100") - self._append_sub("base-motor-temp", self._current_row.set_value) + self._cm.subscribe("base-motor-temp", self._current_row.set_value) - def __prepare_eq(self) -> None: + def __prepare_eq(self): self.add_preferences_page("Equalizer", "network-cellular-signal-excellent-symbolic") self.add_preferences_group("Equalizer") @@ -237,11 +242,11 @@ def __prepare_eq(self) -> None: self._current_row.add_labels("10Hz", "15Hz", "25Hz", "40Hz", "60Hz", "100Hz") self._current_row.set_height(450) for i in range(6): - self._eq_row.subscribe_slider(i, self._cm.set_setting_int, f"base-equalizer{i+1}") - self._append_sub(f"base-equalizer{i+1}", self._eq_row.set_slider_value, i) + self._eq_row.subscribe_slider(i, self._cm.set_setting, f"base-equalizer{i+1}") + self._cm.subscribe(f"base-equalizer{i+1}", self._eq_row.set_slider_value, i) - def __prepare_curve(self) -> None: + def __prepare_curve(self): self.add_preferences_page("Curve", "network-cellular-signal-excellent-symbolic") self.add_preferences_group("Base FFB Curve") @@ -255,35 +260,31 @@ def __prepare_curve(self) -> None: self._current_row.subscribe(self._set_curve_preset) for i in range(5): self._current_row.subscribe_slider(i, self._set_curve_point, i) - self._append_sub(f"base-ffb-curve-y{i+1}", self._get_curve, i) + self._cm.subscribe(f"base-ffb-curve-y{i+1}", self._get_curve, i) self.add_preferences_group("") self._add_row(BoxflatSwitchRow("Force Feedback Reversal")) - self._current_row.subscribe(self._cm.set_setting_int, "base-ffb-reverse") - self._append_sub("base-ffb-reverse", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "base-ffb-reverse") + self._cm.subscribe("base-ffb-reverse", self._current_row.set_value) - def _set_curve_preset(self, value: int) -> None: + def _set_curve_preset(self, value: int): self._set_curve(self._curve_presets[value]) - def _set_curve_point(self, value: int, index: int) -> None: - self._cm.set_setting_int(value, f"base-ffb-curve-y{index+1}") + def _set_curve_point(self, value: int, index: int): + self._cm.set_setting(value, f"base-ffb-curve-y{index+1}") - def _set_curve(self, values: list) -> None: - curve = [] - curve.extend(self._curve_x) - curve.extend(values) + def _set_curve(self, values: list): + for i in range(4): + self._cm.set_setting(self._curve_x[i], f"base-ffb-curve-x{i+1}") - for i in range(1,5): - self._cm.set_setting_int(curve[i-1], f"base-ffb-curve-x{i}") - - for i in range(0,5): - self._cm.set_setting_int(curve[i+4], f"base-ffb-curve-y{i+1}") + for i in range(5): + self._cm.set_setting(values[i], f"base-ffb-curve-y{i+1}") - def _get_curve(self, value: int, sindex: int) -> None: + def _get_curve(self, value: int, sindex: int): index = -1 values = self._curve_row.get_sliders_value() values[sindex] = value @@ -295,6 +296,9 @@ def _get_curve(self, value: int, sindex: int) -> None: self._curve_row.set_slider_value(value, sindex) - def _set_eq_preset(self, index: int) -> None: + def _set_eq_preset(self, index: int, get_index_from_sensitivity=False): + if get_index_from_sensitivity: + index = self._sensitivity_row.get_raw_value() + for i in range(6): - self._cm.set_setting_int(self._eq_presets[index][i], f"base-equalizer{i+1}") + self._cm.set_setting(self._eq_presets[index][i], f"base-equalizer{i+1}") diff --git a/boxflat/panels/h_pattern.py b/boxflat/panels/h_pattern.py index d1c9c0e..e6c23d9 100644 --- a/boxflat/panels/h_pattern.py +++ b/boxflat/panels/h_pattern.py @@ -3,19 +3,19 @@ from boxflat.widgets import * class HPatternSettings(SettingsPanel): - def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager) -> None: + def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager): self._slider1 = None self._slider2 = None super().__init__("H-Pattern Shifter", button_callback, connection_manager) - self._append_sub_connected("hpattern-paddle-sync", self.active) + self._cm.subscribe_connected("hpattern-paddle-sync", self.active) - def active(self, value: int) -> None: + def active(self, value: int): value = -1 if value != 0 else value super().active(value) - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_preferences_group("Warning") self._add_row(BoxflatRow("These options are disabled in FW", "Moza decided that they did't work properly. Only calibration is operational")) @@ -25,26 +25,18 @@ def prepare_ui(self) -> None: row2 = BoxflatSliderRow("Auto Blip Duration", 0, 1000, subtitle="Miliseconds") self._add_row(BoxflatSwitchRow("Auto Downshift Throttle Blip", subtitle="Easy rev match")) - # self._current_row.subscribe(self._cm.set_setting_int, "hpattern-throttle-blip") self._current_row.subscribe(row1.set_active) self._current_row.subscribe(row2.set_active) - # self._append_sub("hpattern-throttle-blip", self._current_row.set_value) self._add_row(row1) self._current_row.add_marks(50) - # self._current_row.subscribe(self._cm.set_setting_int, "hpattern-blip-output") - # self._append_sub("hpattern-blip-output", self._current_row.set_value) - # self._append_sub("hpattern-throttle-blip", self._current_row.set_active) self._current_row.set_active(False) self._add_row(row2) self._current_row.add_marks(250, 500, 750) - # self._current_row.subscribe(self._cm.set_setting_int, "hpattern-blip-duration") - # self._append_sub("hpattern-blip-duration", self._current_row.set_value) - # self._append_sub("hpattern-throttle-blip", self._current_row.set_active) self._current_row.set_active(False) self.add_preferences_group("Calibration") self._add_row(BoxflatCalibrationRow("Device Calibration", "Shift into R > 7th > R > Neutral")) - self._current_row.subscribe(self._cm.set_setting_int, "hpattern") - self._cm.subscribe_shutdown(self._current_row.shutdown) + self._current_row.subscribe("calibration-start", self._cm.set_setting, "hpattern-calibration-start") + self._current_row.subscribe("calibration-stop", self._cm.set_setting, "hpattern-calibration-stop") diff --git a/boxflat/panels/handbrake.py b/boxflat/panels/handbrake.py index 83479e1..fde443b 100644 --- a/boxflat/panels/handbrake.py +++ b/boxflat/panels/handbrake.py @@ -4,7 +4,7 @@ from boxflat.hid_handler import MozaAxis class HandbrakeSettings(SettingsPanel): - def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler) -> None: + def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler): self._threshold_active = None self._calibration_button = None self._curve_row = None @@ -17,32 +17,32 @@ def __init__(self, button_callback: callable, connection_manager: MozaConnection ] super().__init__("Handbrake", button_callback, connection_manager, hid_handler) - self._append_sub_connected("handbrake-direction", self.active) + self._cm.subscribe_connected("handbrake-direction", self.active) - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_preferences_group("Handbrake settings") self._add_row(BoxflatSwitchRow("Reverse Direction")) - self._current_row.subscribe(self._cm.set_setting_int, "handbrake-direction") - self._append_sub("handbrake-direction", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "handbrake-direction") + self._cm.subscribe("handbrake-direction", self._current_row.set_value) row = BoxflatSliderRow("Button threshold", suffix="%") self._add_row(BoxflatToggleButtonRow("Handbrake Mode")) self._current_row.add_buttons("Axis", "Button") - self._current_row.subscribe(self._cm.set_setting_int, "handbrake-mode") + self._current_row.subscribe(self._cm.set_setting, "handbrake-mode") self._current_row.subscribe(row.set_active) - self._append_sub("handbrake-mode", self._current_row.set_value) + self._cm.subscribe("handbrake-mode", self._current_row.set_value) self._add_row(row) self._current_row.add_marks(25, 50, 75) - self._current_row.subscribe(self._cm.set_setting_int, "handbrake-button-threshold") - self._append_sub("handbrake-mode", self._current_row.set_active) - self._append_sub("handbrake-button-threshold", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "handbrake-button-threshold") + self._cm.subscribe("handbrake-mode", self._current_row.set_active) + self._cm.subscribe("handbrake-button-threshold", self._current_row.set_value) self._current_row.set_active(False) self.add_preferences_group("Range settings", level_bar=1) self._current_group.set_bar_max(65535) - self._append_sub_hid(MozaAxis.HANDBRAKE, self._current_group.set_bar_level) + self._hid_handler.subscribe(MozaAxis.HANDBRAKE.name, self._current_group.set_bar_level) self._curve_row = BoxflatEqRow("Output Curve", 5, suffix="%") self._add_row(self._curve_row) @@ -54,46 +54,40 @@ def prepare_ui(self) -> None: self._current_row.subscribe(self._set_curve_preset) for i in range(5): self._curve_row.subscribe_slider(i, self._set_curve_point, i) - self._append_sub(f"handbrake-y{i+1}", self._get_curve, i) + self._cm.subscribe(f"handbrake-y{i+1}", self._get_curve, i) self._add_row(BoxflatSliderRow("Range Start", suffix="%")) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "handbrake-min") - # self._current_row.subscribe(self._current_group.set_range_start) - self._append_sub("handbrake-min", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "handbrake-min") + self._cm.subscribe("handbrake-min", self._current_row.set_value) self._add_row(BoxflatSliderRow("Range End", suffix="%", value=100)) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "handbrake-max") - # self._current_row.subscribe(self._current_group.set_range_end) - self._append_sub("handbrake-max", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "handbrake-max") + self._cm.subscribe("handbrake-max", self._current_row.set_value) self.add_preferences_group("Calibration") self._add_row(BoxflatCalibrationRow("Handbrake Calibration", "Pull handbrake fully once")) - self._current_row.subscribe(self._cm.set_setting_int, "handbrake") - self._cm.subscribe_shutdown(self._current_row.shutdown) + self._current_row.subscribe("calibration-start", self._cm.set_setting, "handbrake-calibration-start") + self._current_row.subscribe("calibration-stop", self._cm.set_setting, "handbrake-calibration-stop") - def _set_curve_preset(self, value: int) -> None: + def _set_curve_preset(self, value: int): self._set_curve(self._presets[value]) - def _set_curve_point(self, value: int, index: int) -> None: - self._cm.set_setting_float(float(value), f"handbrake-y{index+1}") + def _set_curve_point(self, value: int, index: int): + self._cm.set_setting(float(value), f"handbrake-y{index+1}") - def _set_curve(self, values: list) -> None: - curve = [] - curve.extend(values) + def _set_curve(self, values: list): + self._curve_row.set_sliders_value(values, mute=False) - for i in range(0,5): - self._cm.set_setting_float(curve[i], f"handbrake-y{i+1}") - - def _get_curve(self, value: int, sindex: int) -> None: + def _get_curve(self, value: int, sindex: int): index = -1 values = self._curve_row.get_sliders_value() values[sindex] = value diff --git a/boxflat/panels/home.py b/boxflat/panels/home.py index fa58cfb..e42b702 100644 --- a/boxflat/panels/home.py +++ b/boxflat/panels/home.py @@ -4,7 +4,7 @@ from math import ceil, floor class HomeSettings(SettingsPanel): - def __init__(self, button_callback, dry_run: bool, connection_manager, hid_handler, version: str="") -> None: + def __init__(self, button_callback, dry_run: bool, connection_manager, hid_handler, version: str=""): self._test_text = "inactive" if dry_run: self._test_text = "active" @@ -13,42 +13,43 @@ def __init__(self, button_callback, dry_run: bool, connection_manager, hid_handl self._rotation = 180 super().__init__("Home", button_callback, connection_manager=connection_manager, hid_handler=hid_handler) - self._append_sub("base-limit", self._get_rotation_limit) + self._cm.subscribe("base-limit", self._get_rotation_limit) - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_preferences_group("Wheelbase") - self._append_sub_connected("base-limit", self._current_group.set_active) + self._cm.subscribe_connected("base-limit", self._current_group.set_active) self._steer_row = BoxflatLabelRow("Steering position") self._add_row(self._steer_row) self._steer_row.set_suffix("°") self._steer_row.set_subtitle(f"Limit = {self._rotation*2}°") - self._append_sub_hid(MozaAxis.STEERING, self._set_steering) + self._hid_handler.subscribe(MozaAxis.STEERING.name, self._set_steering) + self._current_row.set_value(0) self._add_row(BoxflatButtonRow("Adjust center point", "Center")) - self._current_row.subscribe(self._cm.set_setting_int, "base-calibration") + self._current_row.subscribe(self._cm.set_setting, "base-calibration") self.add_preferences_group("Pedals") - self._append_sub_connected("pedals-throttle-dir", self._current_group.set_active, 1) + self._cm.subscribe_connected("pedals-throttle-dir", self._current_group.set_active, 1) self._add_row(BoxflatMinMaxLevelRow("Throttle input", self._set_limit, "pedals-throttle", max_value=65534)) - self._append_sub_hid(MozaAxis.THROTTLE, self._current_row.set_value) - self._append_sub_connected("pedals-throttle-dir", self._current_row.set_active, 1) + self._hid_handler.subscribe(MozaAxis.THROTTLE.name, self._current_row.set_value) + self._cm.subscribe_connected("pedals-throttle-dir", self._current_row.set_active, 1) self._add_row(BoxflatMinMaxLevelRow("Brake input", self._set_limit, "pedals-brake", max_value=65534)) - self._append_sub_hid(MozaAxis.BRAKE, self._current_row.set_value) - self._append_sub_connected("pedals-throttle-dir", self._current_row.set_active, 1) + self._hid_handler.subscribe(MozaAxis.BRAKE.name, self._current_row.set_value) + self._cm.subscribe_connected("pedals-throttle-dir", self._current_row.set_active, 1) self._add_row(BoxflatMinMaxLevelRow("Clutch input", self._set_limit, "pedals-clutch", max_value=65534)) - self._append_sub_hid(MozaAxis.CLUTCH, self._current_row.set_value) - self._append_sub_connected("pedals-throttle-dir", self._current_row.set_active, 1) + self._hid_handler.subscribe(MozaAxis.CLUTCH.name, self._current_row.set_value) + self._cm.subscribe_connected("pedals-throttle-dir", self._current_row.set_active, 1) self.add_preferences_group("Handbrake") self._add_row(BoxflatMinMaxLevelRow("Input", self._set_limit, "handbrake", max_value=65534)) - self._append_sub_hid(MozaAxis.HANDBRAKE, self._current_row.set_value) - self._append_sub_connected("handbrake-direction", self._current_group.set_present, 1) + self._hid_handler.subscribe(MozaAxis.HANDBRAKE.name, self._current_row.set_value) + self._cm.subscribe_connected("handbrake-direction", self._current_group.set_present, 1) self._current_group.set_present(False) @@ -65,7 +66,7 @@ def prepare_ui(self) -> None: # self._add_row(BoxflatRow(f"Test mode: {self._test_text}")) - def _get_rotation_limit(self, value: int) -> None: + def _get_rotation_limit(self, value: int): if value == self._rotation: return @@ -73,14 +74,14 @@ def _get_rotation_limit(self, value: int) -> None: self._steer_row.set_subtitle(f"Limit = {value*2}°") - def _set_steering(self, value: int) -> None: + def _set_steering(self, value: int): self._steer_row.set_value(round((value - 32768) / 32768 * self._rotation)) def _set_limit(self, fraction_method: callable, command: str, min_max: str): fraction = fraction_method() - current_raw_output = int(self._cm.get_setting_auto(command + "-output")) / 65535 * 100 + current_raw_output = int(self._cm.get_setting(command + "-output")) / 65535 * 100 new_limit = 0 if min_max == "max": @@ -92,4 +93,4 @@ def _set_limit(self, fraction_method: callable, command: str, min_max: str): # print(f"Current raw output: {current_raw_output}") # print(f"New limit: {new_limit}") - self._cm.set_setting_auto(new_limit, f"{command}-{min_max}") + self._cm.set_setting(new_limit, f"{command}-{min_max}") diff --git a/boxflat/panels/others.py b/boxflat/panels/others.py index 340e53b..2305741 100644 --- a/boxflat/panels/others.py +++ b/boxflat/panels/others.py @@ -3,46 +3,43 @@ from boxflat.widgets import * from boxflat.bitwise import * from threading import Thread, Event +from gi.repository import Gtk class OtherSettings(SettingsPanel): - def __init__(self, button_callback: callable, cm: MozaConnectionManager, hid_handler): - self._brake_calibration = None + def __init__(self, button_callback: callable, cm: MozaConnectionManager, hid_handler, version: str): super().__init__("Other", button_callback, connection_manager=cm, hid_handler=hid_handler) + self._version = version + self._register_event("brake-calibration-active") - def subscribe_brake_calibration(self, callback: callable): - self._brake_calibration.subscribe(callback) - - - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_preferences_group("Other settings") self._add_row(BoxflatSwitchRow("Base Bluetooth", "For the mobile app")) self._current_row.reverse_values() self._current_row.set_expression("*85") self._current_row.set_reverse_expression("/85") - self._current_row.subscribe(self._cm.set_setting_int, "main-set-ble-mode") - self._append_sub("main-get-ble-mode", self._current_row.set_value) - self._append_sub_connected("base-limit", self._current_row.set_active) + self._current_row.subscribe(self._cm.set_setting, "main-set-ble-mode") + self._cm.subscribe("main-get-ble-mode", self._current_row.set_value) + self._cm.subscribe_connected("base-limit", self._current_row.set_active) self._add_row(BoxflatSwitchRow("Base FH5 compatibility mode", "Changes USB product ID")) - self._current_row.subscribe(self._cm.set_setting_int, "main-set-compat-mode") - self._append_sub("main-get-compat-mode", self._current_row.set_value) - self._append_sub_connected("main-get-compat-mode", self._current_row.set_present, +1) + self._current_row.subscribe(self._cm.set_setting, "main-set-compat-mode") + self._cm.subscribe("main-get-compat-mode", self._current_row.set_value) + self._cm.subscribe_connected("main-get-compat-mode", self._current_row.set_present, +1) self._add_row(BoxflatSwitchRow("Pedals FH5 compatibility mode", "Changes USB product ID")) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-compat-mode") - self._append_sub("pedals-compat-mode", self._current_row.set_value) - self._append_sub_connected("pedals-compat-mode", self._current_row.set_present, +1) + self._current_row.subscribe(self._cm.set_setting, "pedals-compat-mode") + self._cm.subscribe("pedals-compat-mode", self._current_row.set_value) + self._cm.subscribe_connected("pedals-compat-mode", self._current_row.set_present, +1) self.add_preferences_group("Application settings") + self._add_row(BoxflatSwitchRow("Enable Brake Calibration", "Do it at your own risk")) + self._current_row.subscribe(self._dispatch, "brake-calibration-active") - self._brake_calibration = BoxflatSwitchRow("Enable Brake Calibration", "Do it at your own risk") - self._add_row(self._brake_calibration) - - self._add_row(BoxflatSwitchRow("Read settings continuously")) - self._current_row.subscribe(self._cm.refresh_cont) - self._current_row.set_value(1, mute=False) + # self._add_row(BoxflatSwitchRow("Read settings continuously")) + # self._current_row.subscribe(self._cm.refresh_cont) + # self._current_row.set_value(1, mute=False) self._add_row(BoxflatButtonRow("Refresh Devices", "Refresh", subtitle="Not necessary normally")) self._current_row.subscribe(self._cm.device_discovery) @@ -52,31 +49,38 @@ def prepare_ui(self) -> None: self._current_row.subscribe(self._hid_handler.set_update_rate) self._current_row.set_value(self._hid_handler.get_update_rate()) - # self.add_preferences_group("Custom command") - # self._command = Adw.EntryRow() - # self._command.set_title("Command name") - # self._value = Adw.EntryRow() - # self._value.set_title("Value") + def enable_custom_commands(self): + self.add_preferences_group("Custom command") + self._command = Adw.EntryRow() + self._command.set_title("Command name") + + self._value = Adw.EntryRow() + self._value.set_title("Value") - # read = BoxflatButtonRow("Execute command", "Read") - # write = BoxflatButtonRow("Execute command", "Write") + commands_url = self._version.removesuffix("-flatpak") + #commands_url = "https://raw.githubusercontent.com/Lawstorant/boxflat/refs/heads/main/data/serial.yml" + commands_url = f"https://raw.githubusercontent.com/Lawstorant/boxflat/refs/tags/{commands_url}/data/serial.yml" + #commands_url = f"https://github.com/Lawstorant/boxflat/blob/{commands_url}/data/serial.yml" - # read.subscribe(self._read_custom) - # write.subscribe(self._write_custom) + read = BoxflatButtonRow("Execute command") + read.add_button("Read", self._read_custom) + read.add_button("Write", self._write_custom) + read.add_button("Database", self.open_url, commands_url) - # self._add_row(self._command) - # self._add_row(self._value) - # self._add_row(read) - # self._add_row(write) + self._add_row(self._command) + self._add_row(self._value) + self._add_row(read) - def _read_custom(self, *args) -> None: - out = self._cm.get_setting_int(self._command.get_text()) + def _read_custom(self, *args): + out = self._cm.get_setting(self._command.get_text()) + if out == -1: + out = "Error/Command not found" self._value.set_text(str(out)) - def _write_custom(self, *args) -> None: + def _write_custom(self, *args): com = self._command.get_text() - val = int(self._value.get_text()) - self._cm.set_setting_int(val, com) + val = eval(self._value.get_text()) + self._cm.set_setting(val, com) diff --git a/boxflat/panels/pedals.py b/boxflat/panels/pedals.py index ddf8915..2f490c5 100644 --- a/boxflat/panels/pedals.py +++ b/boxflat/panels/pedals.py @@ -4,7 +4,7 @@ from boxflat.hid_handler import MozaAxis class PedalsSettings(SettingsPanel): - def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler) -> None: + def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler): self._brake_calibration_row = None self._curve_rows = [] @@ -22,21 +22,21 @@ def __init__(self, button_callback: callable, connection_manager: MozaConnection ] super().__init__("Pedals", button_callback, connection_manager, hid_handler) - self._append_sub_connected("pedals-throttle-dir", self.active) + self._cm.subscribe_connected("pedals-throttle-dir", self.active) def set_brake_calibration_active(self, active: bool): self._brake_calibration_row.set_active(active) - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_view_stack() # Throttle self.add_preferences_page("Throttle") self.add_preferences_group("Throttle settings", level_bar=1) self._current_group.set_bar_max(65534) - self._append_sub_hid(MozaAxis.THROTTLE, self._current_group.set_bar_level) + self._hid_handler.subscribe(MozaAxis.THROTTLE.name, self._current_group.set_bar_level) self._curve_rows.append(BoxflatEqRow("Throttle Curve", 5, suffix="%")) self._add_row(self._curve_rows[0]) @@ -48,34 +48,34 @@ def prepare_ui(self) -> None: self._current_row.subscribe(self._set_curve_preset, "throttle") for i in range(5): self._curve_rows[0].subscribe_slider(i, self._set_curve_point, i, "throttle") - self._append_sub(f"pedals-throttle-y{i+1}", self._get_curve, i, "throttle") + self._cm.subscribe(f"pedals-throttle-y{i+1}", self._get_curve, i, "throttle") self._add_row(BoxflatSliderRow("Range Start", suffix="%")) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-throttle-min") - self._append_sub("pedals-throttle-min", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "pedals-throttle-min") + self._cm.subscribe("pedals-throttle-min", self._current_row.set_value) self._add_row(BoxflatSliderRow("Range End", suffix="%")) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-throttle-max") - self._append_sub("pedals-throttle-max", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "pedals-throttle-max") + self._cm.subscribe("pedals-throttle-max", self._current_row.set_value) self.add_preferences_group("Misc") self._add_row(BoxflatSwitchRow("Reverse Direction")) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-throttle-dir") - self._append_sub("pedals-throttle-dir", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "pedals-throttle-dir") + self._cm.subscribe("pedals-throttle-dir", self._current_row.set_value) self._add_row(BoxflatCalibrationRow("Calibration", "Fully depress throttle once")) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-throttle") - self._cm.subscribe_shutdown(self._current_row.shutdown) + self._current_row.subscribe("calibration-start", self._cm.set_setting, "pedals-throttle-calibration-start") + self._current_row.subscribe("calibration-stop", self._cm.set_setting, "pedals-throttle-calibration-stop") # Brake self.add_preferences_page("Brake") self.add_preferences_group("Brake settings", level_bar=1) self._current_group.set_bar_max(65534) - self._append_sub_hid(MozaAxis.BRAKE, self._current_group.set_bar_level) + self._hid_handler.subscribe(MozaAxis.BRAKE.name, self._current_group.set_bar_level) self._curve_rows.append(BoxflatEqRow("Brake Curve", 5, suffix="%")) self._add_row(self._curve_rows[1]) @@ -87,41 +87,41 @@ def prepare_ui(self) -> None: self._current_row.subscribe(self._set_curve_preset, "brake") for i in range(5): self._curve_rows[1].subscribe_slider(i, self._set_curve_point, i, "brake") - self._append_sub(f"pedals-brake-y{i+1}", self._get_curve, i, "brake") + self._cm.subscribe(f"pedals-brake-y{i+1}", self._get_curve, i, "brake") self._add_row(BoxflatSliderRow("Range Start", suffix="%")) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-brake-min") - self._append_sub("pedals-brake-min", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "pedals-brake-min") + self._cm.subscribe("pedals-brake-min", self._current_row.set_value) self._add_row(BoxflatSliderRow("Range End", suffix="%")) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-brake-max") - self._append_sub("pedals-brake-max", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "pedals-brake-max") + self._cm.subscribe("pedals-brake-max", self._current_row.set_value) - self._add_row(BoxflatSliderRow("Angle sensor ratio", suffix="%", subtitle="0% = Only Angle Sensor\n100% = Only Load Cell")) + self._add_row(BoxflatSliderRow("Sensor ratio", suffix="%", subtitle="0% = Only Angle Sensor\n100% = Only Load Cell")) self._current_row.add_marks(25, 50, 75) - self._current_row.subscribe(self._cm.set_setting_float, "pedals-brake-angle-ratio") - self._append_sub("pedals-brake-angle-ratio", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "pedals-brake-angle-ratio") + self._cm.subscribe("pedals-brake-angle-ratio", self._current_row.set_value) self.add_preferences_group("Misc") self._add_row(BoxflatSwitchRow("Reverse Direction")) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-brake-dir") - self._append_sub("pedals-brake-dir", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "pedals-brake-dir") + self._cm.subscribe("pedals-brake-dir", self._current_row.set_value) self._brake_calibration_row = BoxflatCalibrationRow("Calibration", "Fully depress brake once") self._add_row(self._brake_calibration_row) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-brake") + self._current_row.subscribe("calibration-start", self._cm.set_setting, "pedals-throttle-brake-start") + self._current_row.subscribe("calibration-stop", self._cm.set_setting, "pedals-throttle-brake-stop") self._current_row.set_active(False) - self._cm.subscribe_shutdown(self._current_row.shutdown) # Clutch self.add_preferences_page("Clutch") self.add_preferences_group("Clutch settings", level_bar=1) self._current_group.set_bar_max(65534) - self._append_sub_hid(MozaAxis.CLUTCH, self._current_group.set_bar_level) + self._hid_handler.subscribe(MozaAxis.CLUTCH.name, self._current_group.set_bar_level) self._curve_rows.append(BoxflatEqRow("Clutch Curve", 5, suffix="%")) self._add_row(self._curve_rows[2]) @@ -133,47 +133,43 @@ def prepare_ui(self) -> None: self._current_row.subscribe(self._set_curve_preset, "clutch") for i in range(5): self._curve_rows[2].subscribe_slider(i, self._set_curve_point, i, "clutch") - self._append_sub(f"pedals-clutch-y{i+1}", self._get_curve, i, "clutch") + self._cm.subscribe(f"pedals-clutch-y{i+1}", self._get_curve, i, "clutch") self._add_row(BoxflatSliderRow("Range Start", suffix="%")) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-clutch-min") - self._append_sub("pedals-clutch-min", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "pedals-clutch-min") + self._cm.subscribe("pedals-clutch-min", self._current_row.set_value) self._add_row(BoxflatSliderRow("Range End", suffix="%")) self._current_row.add_marks(20, 40, 60, 80) - self._current_row.set_width(380) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-clutch-max") - self._append_sub("pedals-clutch-max", self._current_row.set_value) + self._current_row.set_slider_width(380) + self._current_row.subscribe(self._cm.set_setting, "pedals-clutch-max") + self._cm.subscribe("pedals-clutch-max", self._current_row.set_value) self.add_preferences_group("Misc") self._add_row(BoxflatSwitchRow("Reverse Direction")) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-clutch-dir") - self._append_sub("pedals-clutch-dir", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "pedals-clutch-dir") + self._cm.subscribe("pedals-clutch-dir", self._current_row.set_value) self._add_row(BoxflatCalibrationRow("Calibration", "Fully depress clutch once")) - self._current_row.subscribe(self._cm.set_setting_int, "pedals-clutch") - self._cm.subscribe_shutdown(self._current_row.shutdown) + self._current_row.subscribe("calibration-start", self._cm.set_setting, "pedals-clutch-calibration-start") + self._current_row.subscribe("calibration-stop", self._cm.set_setting, "pedals-clutch-calibration-stop") - def _set_curve_preset(self, value: int, pedal: str) -> None: + def _set_curve_preset(self, value: int, pedal: str): self._set_curve(self._presets[value], pedal) - def _set_curve_point(self, value: int, index: int, pedal: str) -> None: - self._cm.set_setting_float(value, f"pedals-{pedal}-y{index+1}") + def _set_curve_point(self, value: int, index: int, pedal: str): + self._cm.set_setting(value, f"pedals-{pedal}-y{index+1}") - def _set_curve(self, values: list, pedal: str) -> None: - curve = [] - curve.extend(values) + def _set_curve(self, values: list, pedal: str): + self._curve_rows[self._pedals.index(pedal)].set_sliders_value(values, mute=False) - for i in range(0,5): - self._cm.set_setting_float(curve[i], f"pedals-{pedal}-y{i+1}") - - def _get_curve(self, value: int, sindex: int, pedal: str) -> None: + def _get_curve(self, value: int, sindex: int, pedal: str): index = -1 pi = self._pedals.index(pedal) values = self._curve_rows[pi].get_sliders_value() diff --git a/boxflat/panels/presets.py b/boxflat/panels/presets.py index 368e9c4..5131ce8 100644 --- a/boxflat/panels/presets.py +++ b/boxflat/panels/presets.py @@ -5,7 +5,7 @@ import os class PresetSettings(SettingsPanel): - def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, config_path: str) -> None: + def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, config_path: str, version: str): self._includes = {} self._name_row = Adw.EntryRow() self._name_row.set_title("Preset Name") @@ -17,7 +17,7 @@ def __init__(self, button_callback: callable, connection_manager: MozaConnection self.list_presets() - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_preferences_group("Saving") self._add_row(self._name_row) @@ -27,37 +27,37 @@ def prepare_ui(self) -> None: row = BoxflatSwitchRow("Base") expander.add_row(row) row.set_value(1) - self._append_sub_connected("base-limit", row.set_active, 1, True) + self._cm.subscribe_connected("base-limit", row.set_active, 1, True) self._includes["base"] = row.get_value row = BoxflatSwitchRow("Wheel") expander.add_row(row) row.set_value(1) - self._append_sub_connected("wheel-indicator-mode", row.set_active, 1, True) + self._cm.subscribe_connected("wheel-indicator-mode", row.set_active, 1, True) self._includes["wheel"] = row.get_value row = BoxflatSwitchRow("Wheel Colors") expander.add_row(row) row.set_value(1) - self._append_sub_connected("wheel-indicator-mode", row.set_active, 1, True) + self._cm.subscribe_connected("wheel-indicator-mode", row.set_active, 1, True) self._includes["wheel-colors"] = row.get_value row = BoxflatSwitchRow("Pedals") expander.add_row(row) row.set_value(1) - self._append_sub_connected("pedals-throttle-dir", row.set_active, 1, True) + self._cm.subscribe_connected("pedals-throttle-dir", row.set_active, 1, True) self._includes["pedals"] = row.get_value row = BoxflatSwitchRow("Sequential Shifter") expander.add_row(row) row.set_value(1) - self._append_sub_connected("sequential-paddle-sync", row.set_active, 1, True) + self._cm.subscribe_connected("sequential-paddle-sync", row.set_active, 1, True) self._includes["sequential"] = row.get_value row = BoxflatSwitchRow("Handbrake") expander.add_row(row) row.set_value(1) - self._append_sub_connected("handbrake-direction", row.set_active, 1, True) + self._cm.subscribe_connected("handbrake-direction", row.set_active, 1, True) self._includes["handbrake"] = row.get_value if Adw.get_minor_version() >= 6: @@ -88,8 +88,9 @@ def _save_preset(self, *rest): pm = MozaPresetHandler(self._cm) pm.set_path(self._presets_path) pm.set_name(self._name_row.get_text()) - pm.add_callback(self.list_presets) - pm.add_callback(self._activate_save) + + pm.subscribe(self.list_presets) + pm.subscribe(self._activate_save) for key, method in self._includes.items(): if method(): @@ -98,22 +99,21 @@ def _save_preset(self, *rest): pm.save_preset() - def _activate_save(self): + def _activate_save(self, *rest): GLib.idle_add(self._save_row.set_sensitive, True) def _load_preset(self, preset_name: str, *args): - print(f"\nLoading preset {preset_name}") + print(f"Loading preset {preset_name}") self._name_row.set_text(preset_name.removesuffix(".yml")) - pm = MozaPresetHandler(self._cm) pm.set_path(self._presets_path) pm.set_name(preset_name) pm.load_preset() - def _delete_preset(self, value, preset_name: str, *args): + def _delete_preset(self, preset_name: str, *args): filepath = os.path.join(self._presets_path, preset_name) if not os.path.isfile(filepath): @@ -126,7 +126,7 @@ def _delete_preset(self, value, preset_name: str, *args): self.list_presets() - def list_presets(self): + def list_presets(self, *rest): self.remove_preferences_group(self._presets_list_group) if not os.path.exists(self._presets_path): @@ -143,8 +143,19 @@ def list_presets(self): if os.path.isfile(filepath): row = BoxflatButtonRow(file.removesuffix(".yml")) row.add_button("Load", self._load_preset, file) - row.add_button("Delete").add_css_class("destructive-action") - row.subscribe(self._delete_preset, file) + # row.add_button("Settings", self._show_preset_dialog, file) + row.add_button("Delete", self._delete_preset, file).add_css_class("destructive-action") self._add_row(row) + def _show_preset_dialog(self, file_name: str): + if not file_name: + return + + if file_name == "": + return + + dialog = BoxflatPresetDialog(self._presets_path, file_name) + dialog.subscribe("save", print, "Save preset") + dialog.subscribe("delete", self._delete_preset) + dialog.present() diff --git a/boxflat/panels/sequential.py b/boxflat/panels/sequential.py index 284e2a9..dc3dc70 100644 --- a/boxflat/panels/sequential.py +++ b/boxflat/panels/sequential.py @@ -7,31 +7,32 @@ def __init__(self, button_callback: callable, connection_manager: MozaConnection self._S1 = None self._S2 = None super().__init__("Sequential Shifter", button_callback, connection_manager) - self._append_sub_connected("sequential-paddle-sync", self.active) + self._cm.subscribe_connected("sequential-paddle-sync", self.active) - def active(self, value: int) -> None: + def active(self, value: int): value = -1 if value < 1 else value super().active(value) - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_preferences_group("Shifter Settings") self._add_row(BoxflatSwitchRow("Reverse Shift Direction")) - self._current_row.subscribe(self._cm.set_setting_int, "sequential-direction") - self._append_sub("sequential-direction", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "sequential-direction") + self._cm.subscribe("sequential-direction", self._current_row.set_value) self._add_row(BoxflatSwitchRow("Paddle Shifter Synchronization", subtitle="Use the same buttons as paddle shifters")) self._current_row.set_expression("+1") self._current_row.set_reverse_expression("-1") - self._current_row.subscribe(self._cm.set_setting_int, "sequential-paddle-sync") - self._append_sub("sequential-paddle-sync", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "sequential-paddle-sync") + self._cm.subscribe("sequential-paddle-sync", self._current_row.set_value) self.add_preferences_group("Buttons") self._add_row(BoxflatSliderRow("Button Brightness", 0, 10)) - self._current_row.add_marks(5); - self._current_row.subscribe(self._cm.set_setting_int, "sequential-brightness") - self._append_sub("sequential-brightness", self._current_row.set_value) + self._current_row.add_marks(5) + self._current_row.set_slider_width(290) + self._current_row.subscribe(self._cm.set_setting, "sequential-brightness") + self._cm.subscribe("sequential-brightness", self._current_row.set_value) self._S1 = BoxflatColorPickerRow("S1 Color") self._add_row(self._S1) @@ -41,13 +42,13 @@ def prepare_ui(self) -> None: self._S1.subscribe(self._set_colors) self._S2.subscribe(self._set_colors) - self._append_sub("sequential-colors", self._get_colors) + self._cm.subscribe("sequential-colors", self._get_colors) - def _set_colors(self, *args) -> None: - self._cm.set_setting_list([self._S1.get_value(), self._S2.get_value()], "sequential-colors") + def _set_colors(self, *args): + self._cm.set_setting([self._S1.get_value(), self._S2.get_value()], "sequential-colors") - def _get_colors(self, value: list) -> None: + def _get_colors(self, value: list): self._S1.set_value(int(value[0])) self._S2.set_value(int(value[1])) diff --git a/boxflat/panels/settings_panel.py b/boxflat/panels/settings_panel.py index 65cf51a..1ff6ec1 100644 --- a/boxflat/panels/settings_panel.py +++ b/boxflat/panels/settings_panel.py @@ -6,13 +6,17 @@ from gi.repository import Gtk, Adw from boxflat.connection_manager import MozaConnectionManager -from boxflat.hid_handler import HidHandler +from boxflat.hid_handler import HidHandler, MozaAxis +from boxflat.subscription import EventDispatcher -class SettingsPanel(object): +class SettingsPanel(EventDispatcher): def __init__(self, title: str, button_callback: callable, connection_manager: MozaConnectionManager=None, - hid_handler: HidHandler=None) -> None: + hid_handler: HidHandler=None): + super().__init__() + self._cm = connection_manager + self._hid_handler = hid_handler self._current_page = None self._current_group: BoxflatPreferencesGroup=None @@ -21,11 +25,6 @@ def __init__(self, title: str, button_callback: callable, self._header = None self._groups = [] - self._cm_subs = [] - self._cm_subs_connected = [] - - self._hid_subs = [] - self._hid_handler = hid_handler self._active = True self._shutdown = False @@ -74,31 +73,31 @@ def _prepare_banner(self, title="Banner title", label="") -> Adw.Banner: return banner - def prepare_ui(self) -> None: + def prepare_ui(self): return - def set_setting(self, value) -> None: + def set_setting(self, value): pass def get_setting(self) -> int: return 0 - def show_banner(self, value: bool=True) -> None: + def show_banner(self, value: bool=True): GLib.idle_add(self._banner.set_revealed, value) - def hide_banner(self, *arg) -> None: + def hide_banner(self, *arg): GLib.idle_add(self._banner.set_revealed, False) - def set_banner_title(self, new_title: str) -> None: + def set_banner_title(self, new_title: str): self._banner.set_title(new_title) - def set_banner_label(self, new_label: str) -> None: + def set_banner_label(self, new_label: str): self._banner.set_button_label(new_label) def show_toast(self, title: str, timeout=0): GLib.idle_add(self._toast_overlay.add_toast, Adw.Toast(title=title, timeout=timeout)) - def apply(self, *arg) -> None: + def apply(self, *arg): # self.hide_banner() print(f"Applying {self.title} settings...") @@ -115,11 +114,11 @@ def title(self) -> str: return self._button.get_child().get_label() - def deactivate_button(self) -> None: + def deactivate_button(self): self._button.set_active(False) - def active(self, value: int) -> None: + def active(self, value: int): value = (value > -1) if value == self._active: return @@ -135,13 +134,13 @@ def active(self, value: int) -> None: # self._button.set_visible(value) - def open_url(self, url: str) -> None: + def open_url(self, url: str): launcher = Gtk.UriLauncher() launcher.set_uri(url) launcher.launch() - def add_preferences_page(self, name="", icon="preferences-system-symbolic") -> None: + def add_preferences_page(self, name="", icon="preferences-system-symbolic"): page = Adw.PreferencesPage() self._current_page = page @@ -164,15 +163,11 @@ def add_preferences_group(self, title="", level_bar=False, alt_level_bar=False): def remove_preferences_group(self, group: Adw.PreferencesGroup): if not group: return - GLib.idle_add(self._remove_helper, group) - - - def _remove_helper(self, group: Adw.PreferencesGroup): - self._current_page.remove(group) self._groups.remove(group) + GLib.idle_add(self._current_page.remove, group) - def add_view_stack(self) -> None: + def add_view_stack(self): stack = Adw.ViewStack() self._content.set_content(stack) self._current_stack = stack @@ -183,45 +178,17 @@ def add_view_stack(self) -> None: self._header.set_title_widget(switcher) - def _add_row(self, row: BoxflatRow) -> None: + def _add_row(self, row: BoxflatRow): if self._current_group == None: self.add_preferences_group() self._current_row = row - GLib.idle_add(self._current_group.add, row) - - - def _append_sub(self, *args): - self._cm_subs.append(args) - - - def _append_sub_cont(self, *args): - pass - - - def _append_sub_connected(self, *args): - self._cm_subs_connected.append(args) - - - def _append_sub_hid(self, *args): - self._hid_subs.append(args) - - def activate_subs(self) -> None: - for sub in self._cm_subs: - self._cm.subscribe(*sub) - - - def activate_subs_connected(self) -> None: - for sub in self._cm_subs_connected: - self._cm.subscribe_connected(*sub) - - - def activate_hid_subs(self) -> None: - for sub in self._hid_subs: - self._hid_handler.subscribe_axis(*sub) + if isinstance(row, BoxflatRow): + row.set_width(620) + GLib.idle_add(self._current_group.add, row) - # def deactivate_hid_subs(self) -> None: + # def deactivate_hid_subs(self): # if not self._hid_handler: # return @@ -229,5 +196,5 @@ def activate_hid_subs(self) -> None: # self._hid_handler = None - def shutdown(self, *args) -> None: + def shutdown(self, *args): self._shutdown = True diff --git a/boxflat/panels/wheel.py b/boxflat/panels/wheel.py index 57b23bd..a80daaa 100644 --- a/boxflat/panels/wheel.py +++ b/boxflat/panels/wheel.py @@ -20,7 +20,7 @@ ] class WheelSettings(SettingsPanel): - def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler) -> None: + def __init__(self, button_callback: callable, connection_manager: MozaConnectionManager, hid_handler): self._split = None self._timing_row = None self._timing_preset_row = None @@ -39,15 +39,10 @@ def __init__(self, button_callback: callable, connection_manager: MozaConnection ] super().__init__("Wheel", button_callback, connection_manager, hid_handler) - self._append_sub_connected("wheel-indicator-mode", self.active) - self._cm.subscribe_shutdown(self.shutdown) - self._test_thread = Thread(daemon=True, target=self._wheel_rpm_test) - self._test_event = Event() - self._test_thread.start() - # TODO: rewrite threads so they are not persistent + self._cm.subscribe_connected("wheel-indicator-mode", self.active) - def active(self, value: int) -> None: + def active(self, value: int): new_id = None if value == -1: new_id = self._cm.cycle_wheel_id() @@ -65,7 +60,7 @@ def active(self, value: int) -> None: self.set_banner_title(f"Device disconnected. Trying wheel id: {new_id}...") - def prepare_ui(self) -> None: + def prepare_ui(self): self.add_view_stack() self.add_preferences_page("Wheel") @@ -77,29 +72,29 @@ def prepare_ui(self) -> None: self._current_row.set_expression("+1") self._current_row.set_reverse_expression("-1") - self._current_row.subscribe(self._cm.set_setting_int, "wheel-paddles-mode") - self._append_sub("wheel-paddles-mode", self._current_row.set_value) - self._append_sub_connected("wheel-paddles-mode", self._current_row.set_present) + self._current_row.subscribe(self._cm.set_setting, "wheel-paddles-mode") + self._cm.subscribe("wheel-paddles-mode", self._current_row.set_value) + self._cm.subscribe_connected("wheel-paddles-mode", self._current_row.set_present) self._current_row.set_present(False) level = BoxflatLevelRow("Combined Paddles", max_value=65534) self._add_row(level) - self._append_sub_hid(MozaAxis.COMBINED_PADDLES, self._current_row.set_value) - self._append_sub_connected("wheel-paddles-mode", lambda v: level.set_present(v == 2)) - self._append_sub("wheel-clutch-point", self._current_row.set_offset) + self._hid_handler.subscribe(MozaAxis.COMBINED_PADDLES.name, self._current_row.set_value) + self._cm.subscribe_connected("wheel-paddles-mode", lambda v: level.set_present(v == 2)) + self._cm.subscribe("wheel-clutch-point", self._current_row.set_offset) paddle_mode.subscribe(lambda v: level.set_present(v == 2, skip_cooldown=True)) self._current_row.set_present(0, skip_cooldown=True, trigger_cooldown=False) self._add_row(BoxflatLevelRow("Left Paddle", max_value=65534)) - self._append_sub_hid(MozaAxis.LEFT_PADDLE, self._current_row.set_value) - self._append_sub_connected("wheel-paddles-mode", self._current_row.set_present, -2) + self._hid_handler.subscribe(MozaAxis.LEFT_PADDLE.name, self._current_row.set_value) + self._cm.subscribe_connected("wheel-paddles-mode", self._current_row.set_present, -2) paddle_mode.subscribe(self._current_row.set_present, -2, True) self._current_row.set_present(0, skip_cooldown=True, trigger_cooldown=False) self._add_row(BoxflatLevelRow("Right Paddle", max_value=65534)) - self._append_sub_hid(MozaAxis.RIGHT_PADDLE, self._current_row.set_value) - self._append_sub_connected("wheel-paddles-mode", self._current_row.set_present, -2) + self._hid_handler.subscribe(MozaAxis.RIGHT_PADDLE.name, self._current_row.set_value) + self._cm.subscribe_connected("wheel-paddles-mode", self._current_row.set_present, -2) paddle_mode.subscribe(self._current_row.set_present, -2, True) self._current_row.set_present(0, skip_cooldown=True, trigger_cooldown=False) @@ -109,27 +104,27 @@ def prepare_ui(self) -> None: self._current_row.set_active(False) self._current_row.subtitle = "Left paddle cutoff" self._current_row.add_marks(25, 50, 75) - self._current_row.subscribe(self._cm.set_setting_int, "wheel-clutch-point") + self._current_row.subscribe(self._cm.set_setting, "wheel-clutch-point") self._current_row.subscribe(level.set_offset) paddle_mode.subscribe(lambda v: slider.set_active(v == 2)) - self._append_sub("wheel-clutch-point", self._current_row.set_value) - self._append_sub("wheel-paddles-mode", lambda v: slider.set_active(v == 2)) - self._append_sub_connected("wheel-clutch-point", self._current_row.set_present) + self._cm.subscribe("wheel-clutch-point", self._current_row.set_value) + self._cm.subscribe("wheel-paddles-mode", lambda v: slider.set_active(v == 2)) + self._cm.subscribe_connected("wheel-clutch-point", self._current_row.set_present) self._current_row.set_present(False) self._add_row(BoxflatToggleButtonRow("Rotary Encoder Mode")) self._current_row.add_buttons("Buttons", " Knob ") - self._current_row.subscribe(self._cm.set_setting_int, "wheel-knob-mode") - self._append_sub("wheel-knob-mode", self._current_row.set_value) - self._append_sub_connected("wheel-knob-mode", self._current_row.set_present, +1) + self._current_row.subscribe(self._cm.set_setting, "wheel-knob-mode") + self._cm.subscribe("wheel-knob-mode", self._current_row.set_value) + self._cm.subscribe_connected("wheel-knob-mode", self._current_row.set_present, +1) self._stick_row = BoxflatToggleButtonRow("Left Stick Mode") self._add_row(self._stick_row) self._current_row.add_buttons("Buttons", "D-Pad") self._current_row.set_expression("*256") self._current_row.set_reverse_expression("/256") - self._current_row.subscribe(self._cm.set_setting_int, "wheel-stick-mode") - self._append_sub("wheel-stick-mode", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "wheel-stick-mode") + self._cm.subscribe("wheel-stick-mode", self._current_row.set_value) self.add_preferences_group("Misc") @@ -144,14 +139,15 @@ def prepare_ui(self) -> None: self._current_row.add_switch("Switch Dash Display", "Button 32 + Left/Right") self._current_row.add_switch("Set Wheel Center Point", "Press both paddles and Button 1") self._current_row.subscribe(self._set_combination_settings) - self._append_sub("wheel-key-combination", self._get_combination_settings) + self._cm.subscribe("wheel-key-combination", self._get_combination_settings) self._add_row(BoxflatButtonRow("Wheel RPM test", "Test")) self._current_row.subscribe(self.start_test) - self._add_row(BoxflatCalibrationRow("Calibrate Paddles", "Follow instructions here", alternative=True)) - self._current_row.subscribe(self._calibrate_paddles) - self._cm.subscribe_shutdown(self._current_row.shutdown) + calibration = BoxflatCalibrationRow("Calibrate Paddles", "Follow instructions here", alternative=True) + self._add_row(calibration) + calibration.subscribe("calibration-start", self._calibrate_paddles, 0) + calibration.subscribe("calibration-stop", self._calibrate_paddles, 1) # RPM @@ -162,19 +158,19 @@ def prepare_ui(self) -> None: self._current_row.add_buttons("RPM", "Off", "On ") self._current_row.set_expression("+1") self._current_row.set_reverse_expression("-1") - self._current_row.subscribe(self._cm.set_setting_int, "wheel-indicator-mode") - self._append_sub("wheel-indicator-mode", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "wheel-indicator-mode") + self._cm.subscribe("wheel-indicator-mode", self._current_row.set_value) self._add_row(BoxflatToggleButtonRow("RPM Indicator Display Mode")) self._current_row.add_buttons("Mode 1", "Mode 2") - self._current_row.subscribe(self._cm.set_setting_int, "wheel-set-display-mode") - self._append_sub("wheel-get-display-mode", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "wheel-set-display-mode") + self._cm.subscribe("wheel-get-display-mode", self._current_row.set_value) self._add_row(BoxflatToggleButtonRow("RPM Mode")) self._current_row.add_buttons("Percent", "RPM") - self._current_row.subscribe(self._cm.set_setting_int, "wheel-rpm-mode") + self._current_row.subscribe(self._cm.set_setting, "wheel-rpm-mode") self._current_row.subscribe(self._reconfigure_timings) - self._append_sub("wheel-rpm-mode", self._current_row.set_value) + self._cm.subscribe("wheel-rpm-mode", self._current_row.set_value) self.add_preferences_group("Timings") self._timing_row = BoxflatEqRow("RPM Indicator Timing", 10, "Is it my turn now?", @@ -194,80 +190,81 @@ def prepare_ui(self) -> None: self._add_row(self._timing_row2) self._timing_row2.add_buttons("Early", "Normal", "Late") self._timing_row2.subscribe(self._set_rpm_timings2_preset) + self._timing_row2.set_present(0) for i in range(MOZA_RPM_LEDS): self._timing_row2.add_labels(f"RPM{i+1}", index=i) - self._timing_row2.subscribe_slider(i, self._cm.set_setting_int, f"wheel-rpm-value{i+1}") - self._append_sub(f"wheel-rpm-value{i+1}", self._timing_row2.set_slider_value, i) - self._append_sub(f"wheel-rpm-value10", self._get_rpm_timings2_preset) + self._timing_row2.subscribe_slider(i, self._cm.set_setting, f"wheel-rpm-value{i+1}") + self._cm.subscribe(f"wheel-rpm-value{i+1}", self._timing_row2.set_slider_value, i) + # self._cm.subscribe(f"wheel-rpm-value10", self._get_rpm_timings2_preset) - self._append_sub("wheel-rpm-timings", self._get_rpm_timings) - self._append_sub("wheel-rpm-timings", self._get_rpm_timings_preset) - self._append_sub("wheel-rpm-mode", self._reconfigure_timings) + self._cm.subscribe("wheel-rpm-timings", self._get_rpm_timings) + self._cm.subscribe("wheel-rpm-timings", self._get_rpm_timings_preset) + self._cm.subscribe("wheel-rpm-mode", self._reconfigure_timings) self._add_row(BoxflatSliderRow("Blinking Interval", range_end=1000, subtitle="Miliseconds", increment=50)) self._current_row.add_marks(125, 250, 375, 500, 625, 750, 875) - self._current_row.subscribe(self._cm.set_setting_int, "wheel-rpm-interval") - self._append_sub("wheel-rpm-interval", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "wheel-rpm-interval") + self._cm.subscribe("wheel-rpm-interval", self._current_row.set_value) self.add_preferences_page("Colors") self.add_preferences_group("Buttons") - self._add_row(BoxflatNewColorPickerRow("", blinking=True)) - self._current_row.subscribe(self._cm.set_setting_list, "wheel-button-color") - self._append_sub_connected("wheel-buttons-brightness", self._current_row.set_active, +1) - for i in range(MOZA_RPM_LEDS): - self._append_sub(f"wheel-button-color{i+1}", self._current_row.set_led_value, i) + self._add_row(BoxflatNewColorPickerRow(blinking=True)) + self._cm.subscribe_connected("wheel-buttons-brightness", self._current_row.set_active, +1) + for i in range(MOZA_RGB_BUTTONS): + self._current_row.subscribe(f"color{i}", self._cm.set_setting, f"wheel-button-color{i+1}") + self._cm.subscribe(f"wheel-button-color{i+1}", self._current_row.set_led_value, i) self.add_preferences_group("RPM Colors") - self._add_row(BoxflatNewColorPickerRow("")) - self._current_row.subscribe(self._cm.set_setting_list, "wheel-rpm-color") + self._add_row(BoxflatNewColorPickerRow()) for i in range(MOZA_RPM_LEDS): - self._append_sub(f"wheel-rpm-color{i+1}", self._current_row.set_led_value, i) + self._current_row.subscribe(f"color{i}", self._cm.set_setting, f"wheel-rpm-color{i+1}") + self._cm.subscribe(f"wheel-rpm-color{i+1}", self._current_row.set_led_value, i) self.add_preferences_group("RPM Blinking") - self._add_row(BoxflatNewColorPickerRow("")) - self._current_row.subscribe(self._cm.set_setting_list, "wheel-rpm-blink-color") + self._add_row(BoxflatNewColorPickerRow()) + for i in range(MOZA_RPM_LEDS): + self._current_row.subscribe(f"color{i}", self._cm.set_setting, f"wheel-rpm-blink-color{i+1}") self.add_preferences_group("Brightness") self._add_row(BoxflatSliderRow("Button Brightness", range_end=15)) self._current_row.add_marks(5, 10) - self._current_row.subscribe(self._cm.set_setting_int, "wheel-buttons-brightness") - self._append_sub("wheel-buttons-brightness", self._current_row.set_value) - self._append_sub_connected("wheel-buttons-brightness", self._current_row.set_present, +1) + self._current_row.subscribe(self._cm.set_setting, "wheel-buttons-brightness") + self._cm.subscribe("wheel-buttons-brightness", self._current_row.set_value) + self._cm.subscribe_connected("wheel-buttons-brightness", self._current_row.set_present, +1) self._add_row(BoxflatSliderRow("RPM Brightness", range_end=15)) self._current_row.add_marks(5, 10) - self._current_row.subscribe(self._cm.set_setting_int, "wheel-rpm-brightness") - self._append_sub("wheel-rpm-brightness", self._current_row.set_value) + self._current_row.subscribe(self._cm.set_setting, "wheel-rpm-brightness") + self._cm.subscribe("wheel-rpm-brightness", self._current_row.set_value) # self._add_row(BoxflatSliderRow("Flag Brightness", range_end=15)) # self._current_row.add_marks(5, 10) - # self._current_row.subscribe(self._cm.set_setting_int, "wheel-flags-brightness") - # self._append_sub("wheel-flags-brightness", self._current_row.set_value) + # self._current_row.subscribe(self._cm.set_setting, "wheel-flags-brightness") + # self._cm.subscribe("wheel-flags-brightness", self._current_row.set_value) # self.add_preferences_group("Telemetry flag") # self._add_row(BoxflatNewColorPickerRow("")) - # self._current_row.subscribe(self._cm.set_setting_list, "wheel-flag-color") # for i in range(MOZA_RPM_LEDS): - # self._append_sub(f"wheel-flag-color{i+1}", self._current_row.set_led_value, i) + # self._cm.subscribe(f"wheel-flag-color{i+1}", self._current_row.set_led_value, i) - def _set_rpm_timings(self, timings: list) -> None: - self._cm.set_setting_list(timings, "wheel-rpm-timings") + def _set_rpm_timings(self, timings: list): + self._cm.set_setting(timings, "wheel-rpm-timings") - def _set_rpm_timings_preset(self, value: int) -> None: - self._cm.set_setting_list(self._timings[value], "wheel-rpm-timings") + def _set_rpm_timings_preset(self, value: int): + self._timing_row.set_sliders_value(self._timings[value], mute=False) - def _get_rpm_timings(self, timings: list) -> None: + def _get_rpm_timings(self, timings: list): self._timing_row.set_sliders_value(timings) - def _get_rpm_timings_preset(self, timings: list) -> None: + def _get_rpm_timings_preset(self, timings: list): index = -1 if list(timings) in self._timings: index = self._timings.index(list(timings)) @@ -275,12 +272,11 @@ def _get_rpm_timings_preset(self, timings: list) -> None: self._timing_row.set_button_value(index) - def _set_rpm_timings2_preset(self, index) -> None: - for i in range(10): - self._cm.set_setting_int(self._timings2[index][i], f"wheel-rpm-value{i+1}") + def _set_rpm_timings2_preset(self, index): + self._timing_row2.set_sliders_value(self._timings2[index], mute=False) - def _get_rpm_timings2_preset(self, *args) -> None: + def _get_rpm_timings2_preset(self, *args): index = -1 timings = self._timing_row2.get_sliders_value() @@ -290,21 +286,21 @@ def _get_rpm_timings2_preset(self, *args) -> None: self._timing_row2.set_button_value(index) - def _reconfigure_timings(self, value: int) -> None: + def _reconfigure_timings(self, value: int): self._timing_row.set_present(value < 1) self._timing_row2.set_present(value >= 1) - def _calibrate_paddles(self, value: int) -> None: + def _calibrate_paddles(self, value: int): if value == 0: - self._cm.set_setting_int(5, "wheel-paddles-calibration") + self._cm.set_setting(5, "wheel-paddles-calibration") else: - self._cm.set_setting_int(1, "wheel-paddles-calibration") - self._cm.set_setting_int(2, "wheel-paddles-calibration2") + self._cm.set_setting(1, "wheel-paddles-calibration") + self._cm.set_setting(2, "wheel-paddles-calibration2") - def _set_combination_settings(self, values) -> None: + def _set_combination_settings(self, values): output = self._wheel_combination_data.copy() if len(output) != 4: @@ -319,10 +315,10 @@ def _set_combination_settings(self, values) -> None: output[3] = modify_bit(output[3], 1, values[6]) output[3] = modify_bit(output[3], 5, values[7]) - self._cm.set_setting_list(output, "wheel-key-combination") + self._cm.set_setting(output, "wheel-key-combination") - def _get_combination_settings(self, values) -> None: + def _get_combination_settings(self, values): self._wheel_combination_data = values byte1 = values[1] byte2 = values[3] @@ -344,42 +340,59 @@ def _get_combination_settings(self, values) -> None: self._combination_row.set_value(switch_values) - def start_test(self, *args) -> None: - self._test_event.set() - + def start_test(self, *args): + self._test_thread = Thread(daemon=True, target=self._wheel_rpm_test).start() - def _wheel_rpm_test(self, *args) -> None: - while not self._shutdown: - if not self._test_event.wait(timeout=1): - continue - self._test_event.clear() + def _wheel_rpm_test(self, *args): + self._cm.set_setting(0, "wheel-send-telemetry") + time.sleep(0.2) + initial_mode = self._cm.get_setting("wheel-indicator-mode") + self._cm.set_setting(1, "wheel-indicator-mode") - initial_mode = self._cm.get_setting_int("wheel-indicator-mode") - self._cm.set_setting_int(1, "wheel-indicator-mode") - - t = 0.3 + t = 0.07 + for j in range(2): for i in range(10): - val = modify_bit(0, i) - self._cm.set_setting_int(val, "wheel-send-telemetry") + val = bit(i) + self._cm.set_setting(val, "wheel-send-telemetry") time.sleep(t) - for i in reversed(range(9)): - val = modify_bit(0, i) - self._cm.set_setting_int(val, "wheel-send-telemetry") + for i in reversed(range(1,9)): + val = bit(i) + self._cm.set_setting(val, "wheel-send-telemetry") time.sleep(t) - val = 0 - self._cm.set_setting_int(val, "wheel-send-telemetry") + val = 0 + self._cm.set_setting(val, "wheel-send-telemetry") + time.sleep(t) + for i in range(10): + val = set_bit(val, i) + self._cm.set_setting(val, "wheel-send-telemetry") + time.sleep(t) + + for i in range(9): + val = unset_bit(val, i) + self._cm.set_setting(val, "wheel-send-telemetry") + time.sleep(t) + + for i in reversed(range(10)): + val = set_bit(val, i) + self._cm.set_setting(val, "wheel-send-telemetry") + time.sleep(t) + + for i in reversed(range(1,10)): + val = unset_bit(val, i) + self._cm.set_setting(val, "wheel-send-telemetry") + time.sleep(t) + + for i in range(1,10): + val = set_bit(val, i) + self._cm.set_setting(val, "wheel-send-telemetry") time.sleep(t) - for i in range(10): - val = modify_bit(val, i) - self._cm.set_setting_int(val, "wheel-send-telemetry") - time.sleep(t) - time.sleep(0.5) - val = modify_bit(0,15) - self._cm.set_setting_int(val, "wheel-send-telemetry") - time.sleep(1) + time.sleep(0.2) + val = modify_bit(0,15) + self._cm.set_setting(val, "wheel-send-telemetry") + time.sleep(0.8) - self._cm.set_setting_int(initial_mode, "wheel-indicator-mode") + self._cm.set_setting(initial_mode, "wheel-indicator-mode") diff --git a/boxflat/preset_handler.py b/boxflat/preset_handler.py index abdaccd..fa995f0 100644 --- a/boxflat/preset_handler.py +++ b/boxflat/preset_handler.py @@ -3,6 +3,7 @@ import yaml import os from threading import Thread +from .subscription import SimpleEventDispatcher MozaDevicePresetSettings = { "base" : [ @@ -139,13 +140,13 @@ ] } -class MozaPresetHandler(): +class MozaPresetHandler(SimpleEventDispatcher): def __init__(self, connection_manager: MozaConnectionManager): + super().__init__() self._settings = {} self._cm = connection_manager self._path = None self._name = None - self._callbacks = [] def set_path(self, preset_path: str): @@ -156,10 +157,6 @@ def set_name(self, name: str): self._name = name - def add_callback(self, callback: callable): - self._callbacks.append(callback) - - def append_setting(self, setting_name: str): command = MozaCommand(setting_name, self._cm.get_command_data()) if command.device_type not in self._settings: @@ -204,7 +201,7 @@ def _save_preset(self): while tries < 3: tries += 1 replace = setting.replace("set-", "get-") - value = self._cm.get_setting_auto(f"{device}-{replace}") + value = self._cm.get_setting(f"{device}-{replace}") if value != -1: preset_data[device][setting] = value tries = 3 @@ -217,8 +214,7 @@ def _save_preset(self): with open(os.path.join(path, self._name + ".yml"), "w") as file: file.write(yaml.safe_dump(preset_data)) - for callback in self._callbacks: - callback() + self._dispatch() def _load_preset(self): @@ -233,8 +229,8 @@ def _load_preset(self): for key, settings in preset_data.items(): if key in MozaDevicePresetSettings.keys(): for setting, value in settings.items(): + setting = setting.replace("get-", "set-").replace("-end", "-max").replace("-start", "-min") # print(f"{key}-{setting}: {value}") - self._cm.set_setting_auto(value, f"{key}-{setting}") + self._cm.set_setting(value, f"{key}-{setting}") - for callback in self._callbacks: - callback() + self._dispatch() diff --git a/boxflat/subscription.py b/boxflat/subscription.py new file mode 100644 index 0000000..6dc9955 --- /dev/null +++ b/boxflat/subscription.py @@ -0,0 +1,245 @@ +class Subscription(): + def __init__(self, callback: callable, *args): + self._callback = callback + self._args = args + + + def call(self): + self._callback(*self._args) + + + def call_without_args(self): + self._callback() + + + def call_with_value(self, value): + self._callback(value, *self._args) + + + def call_with_values(self, *values): + self._callback(*values, *self._args) + + + def call_with_custom_args(self, *args): + self._callback(*args) + + + +class SubscriptionList(): + def __init__(self): + self._subscriptions = [] + + + def count(self) -> int: + return len(self._subscriptions) + + + def append(self, callback: callable, *args): + if callable(callback): + self._subscriptions.append(Subscription(callback, *args)) + + + def append_subscription(self, subscription: Subscription): + self._subscriptions.append(subscription) + + + def call(self): + """ + Call all subscribers with default arguments + """ + for sub in self._subscriptions: + sub.call() + + + def call_with_value(self, value): + for sub in self._subscriptions: + sub.call_with_value(value) + + + def call_with_values(self, *values): + for sub in self._subscriptions: + sub.call_with_values(*values) + + + def call_without_args(self): + for sub in self._subscriptions: + sub.call_without_args() + + + def call_with_custom_args(self, *args): + for sub in self._subscriptions: + sub.call_with_custom_args(*args) + + + def clear(self): + self._subscriptions.clear() + + + +class EventDispatcher(): + def __init__(self): + self.__events = {} + + + def _event_sub_count(self, event_name: str) -> int: + if event_name in self.__events: + return self.__events[event_name].count() + return -1 + + + def list_events(self) -> list[str]: + return list(self.__events.keys()) + + + @property + def events(self) -> list[str]: + return self.list_events() + + + def __find_event(self, event_name: str) -> bool: + return bool(self.__events.get(event_name, False)) + + + def _register_event(self, event_name: str) -> bool: + if not self.__find_event(event_name): + self.__events[event_name] = SubscriptionList() + return True + return False + # TODO debug warn if event already exists + + + def _register_events(self, *event_names: str): + for event in event_names: + self._register_event(event) + #print(f"Event \"{event}\" registered") + + + def _deregister_event(self, event_name: str) -> bool: + if event_name not in self.events: + return False + + self.__events.pop(event_name) + return True + + def _deregister_events(self) -> bool: + self.__events = {} + + + def _dispatch(self, event_name: str, *values) -> bool: + if not self.__find_event(event_name): + return False + + if len(values) == 0: + self.__events[event_name].call() + else: + self.__events[event_name].call_with_values(*values) + return True + + + def subscribe(self, event_name: str, callback: callable, *args) -> bool: + if not self.__find_event(event_name): + return False + + self.__events[event_name].append(callback, *args) + return True + + + def _clear_event_subscriptions(self, event_name: str) -> bool: + if not self.__find_event(event_name): + return False + + self.__events[event_name].clear() + + + def _clear_subscriptions(self, event_names=None): + if not event_names: + event_names = self.__events.keys() + + for event in event_names: + self._clear_event_subscriptions(event) + + + +class SimpleEventDispatcher(): + def __init__(self): + self.__events = SubscriptionList() + + + def _dispatch(self, *values): + if len(values) == 0: + self.__events.call() + else: + self.__events.call_with_values(*values) + + + def subscribe(self, callback: callable, *args): + self.__events.append(callback, *args) + + + def _clear_subscriptions(self): + self.__events.clear() + + + +class Observable(SimpleEventDispatcher): + def __init__(self, init_value=None): + super().__init__() + self._value = init_value + + + def __call__(self): + self._dispatch(self._value) + + + @property + def value(self): + return self._value + + + @value.setter + def value(self, new_value): + if new_value != self._value: + self._dispatch(new_value) + self._value = new_value + + +class Semaphore(EventDispatcher): + def __init__(self, maximum: int): + super().__init__() + self._value = 0 + self._max = maximum + self._register_event("value-changed") + self._register_event("quorum-established") + self._register_event("quorum-dissolved") + + + @property + def value(self): + return self._value + + + @value.setter + def value(self, new_value: int): + if new_value > self._max: + return + + if new_value < 0: + return + + old_value = self._value + self._value = new_value + + if new_value != old_value: + self._dispatch(new_value) + if new_value == self._max: + self._dispatch("quorum-established") + elif old_value == self._max: + self._dispatch("quorum-dissolved") + + + def increment(self): + self.value += 1 + + + def decrement(self): + self.value -= 1 diff --git a/boxflat/widgets/__init__.py b/boxflat/widgets/__init__.py index 935651e..09ddbc5 100644 --- a/boxflat/widgets/__init__.py +++ b/boxflat/widgets/__init__.py @@ -14,3 +14,5 @@ from .level_row import * from .button_level_row import * from .min_max_level_row import * +from .preset_dialog import * +from .advance_row import * diff --git a/boxflat/widgets/advance_row.py b/boxflat/widgets/advance_row.py new file mode 100644 index 0000000..e42ee33 --- /dev/null +++ b/boxflat/widgets/advance_row.py @@ -0,0 +1,20 @@ +from gi.repository import Gtk, GLib +from . import BoxflatRow + +class BoxflatAdvanceRow(BoxflatRow): + def __init__(self, title="", subtitle=""): + super().__init__(title=title, subtitle=subtitle) + + self._icon = Gtk.Image() + self._icon.set_from_icon_name("go-next-symbolic") + self._icon.set_valign(Gtk.Align.CENTER) + + self._set_widget(self._icon) + self.set_activatable(True) + + self.connect("activated", self._notify) + + + def set_active(self, value=1, offset=0): + super().set_active(value=value, offset=offset) + GLib.idle_add(self._icon.set_opacity, 0.5 + int(self._active)) diff --git a/boxflat/widgets/button_level_row.py b/boxflat/widgets/button_level_row.py index b64a92a..61c1ec6 100644 --- a/boxflat/widgets/button_level_row.py +++ b/boxflat/widgets/button_level_row.py @@ -20,10 +20,10 @@ def add_button(self, button_label: str, callback: callable, *args) -> Gtk.Button button = Gtk.Button(label=button_label) button.set_valign(Gtk.Align.CENTER) button.set_margin_start(10) - self._box.append(button) button.add_css_class("level-button") - button.connect('clicked', lambda button: callback(*args)) + + self._box.append(button) return button diff --git a/boxflat/widgets/button_row.py b/boxflat/widgets/button_row.py index 85a231b..0ef119f 100644 --- a/boxflat/widgets/button_row.py +++ b/boxflat/widgets/button_row.py @@ -22,12 +22,11 @@ def get_value(self) -> int: def add_button(self, button_label: str, callback: callable=None, *args) -> Gtk.Button: button = Gtk.Button(label=button_label) button.set_valign(Gtk.Align.CENTER) - # button.set_margin_start(10) self._box.append(button) if callback: button.connect('clicked', lambda button: callback(*args)) else: - button.connect('clicked', lambda button: self._notify()) + button.connect('clicked', self._notify) return button diff --git a/boxflat/widgets/calibration_row.py b/boxflat/widgets/calibration_row.py index 0d124c7..2f33952 100644 --- a/boxflat/widgets/calibration_row.py +++ b/boxflat/widgets/calibration_row.py @@ -1,70 +1,44 @@ -import gi -gi.require_version('Gtk', '4.0') from gi.repository import Gtk, GLib from .button_row import BoxflatButtonRow from threading import Thread -from threading import Event from time import sleep +from boxflat.subscription import EventDispatcher -class BoxflatCalibrationRow(BoxflatButtonRow): +class BoxflatCalibrationRow(EventDispatcher, BoxflatButtonRow): def __init__(self, title: str, subtitle="", alternative=False): - super().__init__(title, "Calibrate", subtitle) - self._alternative = alternative - self._calibration_event = Event() - self._thread = Thread(daemon=True, target=self._calibration) - self._thread.start() - - - def _notify(self) -> None: - self._calibration_event.set() - - - def _notify_calibration(self) -> None: - if self._mute: - return - - self._cooldown = 1 - for sub in self._subscribers: - if not self._alternative: - sub[0](1, f"{sub[2][0]}-{self.get_value()}-calibration") - - else: - sub[0](self.get_value(), *sub[2]) + BoxflatButtonRow.__init__(self, title, button_label="Calibrate", subtitle=subtitle) + EventDispatcher.__init__(self) + self._alternative = alternative + self._register_events("calibration-start", "calibration-stop") - def get_value(self) -> str: - if self._calibration_event.is_set(): - return 1 if self._alternative else "start" - return 0 if self._alternative else "stop" + def _notify(self, *rest): + Thread(daemon=True, target=self._calibration).start() - def _calibration(self) -> None: - while not self._shutdown: - if not self._calibration_event.wait(timeout=1): - continue - GLib.idle_add(self.set_active, False) - tmp = self.get_subtitle() - text = "Calibration in progress..." - print("Calibration start") + def _calibration(self): + GLib.idle_add(self.set_active, False) + tmp = self.get_subtitle() + text = "Calibration in progress..." + print("Calibration start") - if self._alternative: - GLib.idle_add(self.set_subtitle, "Press all paddles!") - sleep(4) + if self._alternative: + GLib.idle_add(self.set_subtitle, "Press all paddles!") + sleep(4) - self._notify_calibration() + self._dispatch("calibration-start") - for i in reversed(range(3 if self._alternative else 8)): - GLib.idle_add(self.set_subtitle, f"{text} {i+1}s") - sleep(1) + for i in reversed(range(3 if self._alternative else 8)): + GLib.idle_add(self.set_subtitle, f"{text} {i+1}s") + sleep(1) - if self._alternative: - GLib.idle_add(self.set_subtitle, "Release paddles") - sleep(3) + if self._alternative: + GLib.idle_add(self.set_subtitle, "Release paddles") + sleep(3) - self._calibration_event.clear() - self._notify_calibration() - print("Calibration stop") + self._dispatch("calibration-stop") + print("Calibration stop") - GLib.idle_add(self.set_subtitle, tmp) - GLib.idle_add(self.set_active, True) + GLib.idle_add(self.set_subtitle, tmp) + GLib.idle_add(self.set_active, True) diff --git a/boxflat/widgets/color_picker_row.py b/boxflat/widgets/color_picker_row.py index 2251973..1862b5a 100644 --- a/boxflat/widgets/color_picker_row.py +++ b/boxflat/widgets/color_picker_row.py @@ -6,9 +6,11 @@ class BoxflatColorPickerRow(BoxflatToggleButtonRow): def __init__(self, title: str, subtitle="", alt_colors=False): super().__init__(title, subtitle) - for i in range(0, PICKER_COLORS): + for i in range(PICKER_COLORS): self.add_buttons(str(i)) self._buttons[i].add_css_class("color-button") self._buttons[i].add_css_class(f"c{i}") self._buttons[i].connect('toggled', lambda b: b.add_css_class("cs") if b.get_active() == True else b.remove_css_class("cs")) + + self.set_value(0) diff --git a/boxflat/widgets/combo_row.py b/boxflat/widgets/combo_row.py index 5e7bb50..ae2dbc8 100644 --- a/boxflat/widgets/combo_row.py +++ b/boxflat/widgets/combo_row.py @@ -17,7 +17,7 @@ # def row_name(self) -> str: # return self._row_name -# def add_combo_row(self, title: str, values: dict, callback=None, subtitle="") -> None: +# def add_combo_row(self, title: str, values: dict, callback=None, subtitle=""): # if self._current_group == None: # return diff --git a/boxflat/widgets/dialog_row.py b/boxflat/widgets/dialog_row.py index 3f77fb7..8cbfb30 100644 --- a/boxflat/widgets/dialog_row.py +++ b/boxflat/widgets/dialog_row.py @@ -1,5 +1,3 @@ -import gi -gi.require_version('Gtk', '4.0') from gi.repository import Gtk, Adw from .row import BoxflatRow from .switch_row import BoxflatSwitchRow @@ -27,7 +25,7 @@ def __init__(self, title: str, subtitle=""): self.connect("activated", self.show_dialog) - def show_dialog(self, whatever) -> None: + def show_dialog(self, whatever): if self._page.get_parent(): return @@ -38,24 +36,23 @@ def show_dialog(self, whatever) -> None: dialog.present() - def add_switches(self, *switches) -> None: + def add_switches(self, *switches): for switch in switches: self.add_switch(switch) - def add_switch(self, title: str, subtitle="") -> None: + def add_switch(self, title: str, subtitle=""): row = BoxflatSwitchRow(title, subtitle=subtitle) row.set_width(400) self._group.add(row) self._switches.append(row) - row.subscribe(lambda v: self._notify()) + row.subscribe(self._notify) def get_value(self) -> list: values = [] for switch in self._switches: values.append(switch.get_value()) - return values @@ -63,8 +60,6 @@ def get_count(self) -> int: return len(self._switches) - def set_value(self, values) -> None: + def _set_value(self, values): for i in range(self.get_count()): self._switches[i].set_value(bool(values[i])) - - return values diff --git a/boxflat/widgets/equalizer_row.py b/boxflat/widgets/equalizer_row.py index 223265c..564899e 100644 --- a/boxflat/widgets/equalizer_row.py +++ b/boxflat/widgets/equalizer_row.py @@ -1,8 +1,7 @@ -import gi -gi.require_version('Gtk', '4.0') -from gi.repository import Gtk +from gi.repository import Gtk, GLib from .row import BoxflatRow from .toggle_button_row import BoxflatToggleButtonRow +from boxflat.subscription import SubscriptionList class BoxflatEqRow(BoxflatToggleButtonRow): def __init__(self, title: str, sliders_number: int, @@ -12,8 +11,8 @@ def __init__(self, title: str, sliders_number: int, super().__init__(title, subtitle) self._suffix = suffix - self._slider_subs = [] - self._sliders_subs = [] + self._slider_subs_list = [] + self._sliders_subs = SubscriptionList() self._button_row = button_row self._draw_marks = draw_marks @@ -35,7 +34,7 @@ def __init__(self, title: str, sliders_number: int, for i in range(sliders_number): slider = Gtk.Scale(orientation=Gtk.Orientation.VERTICAL) self._sliders.append(slider) - self._slider_subs.append([]) + self._slider_subs_list.append(SubscriptionList()) slider.set_range(range_start, range_end) slider.set_inverted(True) slider.set_halign(Gtk.Align.FILL) @@ -75,17 +74,16 @@ def __init__(self, title: str, sliders_number: int, self.add_marks(range_start, range_end) - def add_marks(self, *marks: int) -> None: + def add_marks(self, *marks: int): if not self._draw_marks: return for mark in marks: for slider in self._sliders: - slider.add_mark(mark, - Gtk.PositionType.RIGHT,f"{mark}{self._suffix}") + slider.add_mark(mark, Gtk.PositionType.RIGHT,f"{mark}{self._suffix}") - def add_labels(self, *labels: str, index=None) -> None: + def add_labels(self, *labels: str, index=None): if index != None: self._slider_labels[index].set_text(labels[0]) return @@ -94,13 +92,13 @@ def add_labels(self, *labels: str, index=None) -> None: self._slider_labels[i].set_text(labels[i]) - def add_buttons(self, *buttons) -> None: + def add_buttons(self, *buttons): super().add_buttons(*buttons) for button in self._buttons: button.set_hexpand(self._button_row) - def set_height(self, height: int) -> None: + def set_height(self, height: int): for slider in self._sliders: slider.set_size_request(0, height) @@ -117,17 +115,19 @@ def get_slider_value(self, index: int) -> int: return round(self._sliders[index].get_value()) - def set_sliders_value(self, values: list) -> None: - self.mute(True) + def set_sliders_value(self, values: list, mute=True): if len(values) > len(self._sliders): return for i in range(len(values)): - self._sliders[i].set_value(values[i]) - self.unmute() + self.set_slider_value(values[i], i, mute) + + def set_slider_value(self, value: int, index: int, mute=True): + GLib.idle_add(self._set_slider_value, value, index, mute) - def set_slider_value(self, value: int, index: int, mute=True) -> None: + + def _set_slider_value(self, value: int, index: int, mute=True): self.mute(mute) self._sliders[index].set_value(value) self.unmute() @@ -137,45 +137,43 @@ def get_button_value(self) -> int: return super().get_value() - def set_button_value(self, value: int, mute: bool=True) -> None: + def set_button_value(self, value: int, mute: bool=True): super().set_value(value, mute) - def subscribe_slider(self, index: int, callback: callable, *args) -> None: - self._slider_subs[index].append((callback, args)) + def subscribe_slider(self, index: int, callback: callable, *args): + self._slider_subs_list[index].append(callback, *args) - def subscribe_sliders(self, callback: callable, *args) -> None: - self._sliders_subs.append((callback, args)) + def subscribe_sliders(self, callback: callable, *args): + self._sliders_subs.append(callback, *args) - def set_slider_active(self, active: bool, index: int) -> None: + def set_slider_active(self, active: bool, index: int): self._sliders[index].set_sensitive(active) - def _notify_slider(self, scale) -> None: - if self._mute: + def _notify_slider(self, scale: Gtk.Scale): + if self._mute.is_set(): return - self._cooldown = 1 + self._cooldown = 2 index = self._sliders.index(scale) self.set_button_value(-1) - for sub in self._slider_subs[index]: - sub[0](self.get_slider_value(index), *sub[1]) + self._slider_subs_list[index].call_with_value(self.get_slider_value(index)) - def _notify_sliders(self, scale) -> None: - if self._mute: + def _notify_sliders(self, scale): + if self._mute.is_set(): return self._cooldown = 1 self.set_button_value(-1) - for sub in self._sliders_subs: - sub[0](self.get_sliders_value(), *sub[1]) + self._sliders_subs.call_with_value(self.get_sliders_value()) - def reconfigure(self, range_start=0, range_end=100, clear_marks=True) -> None: + def reconfigure(self, range_start=0, range_end=100, clear_marks=True): for slider in self._sliders: slider.set_range(range_start, range_end) if clear_marks: diff --git a/boxflat/widgets/label_row.py b/boxflat/widgets/label_row.py index a7a7b88..9a129a5 100644 --- a/boxflat/widgets/label_row.py +++ b/boxflat/widgets/label_row.py @@ -15,10 +15,10 @@ def __init__(self, title: str, subtitle="", value=""): self._suffix = "" - def _set_value(self, value: str) -> None: + def _set_value(self, value: str): value = round(eval("value"+self._reverse_expression), 1) - GLib.idle_add(self._label.set_label, str(value) + self._suffix) + self._label.set_label(str(value) + self._suffix) - def set_suffix(self, suffix: str) -> None: + def set_suffix(self, suffix: str): self._suffix = str(suffix) diff --git a/boxflat/widgets/level_row.py b/boxflat/widgets/level_row.py index 38d4517..b47a92d 100644 --- a/boxflat/widgets/level_row.py +++ b/boxflat/widgets/level_row.py @@ -1,5 +1,3 @@ -import gi -gi.require_version('Gtk', '4.0') from gi.repository import Gtk, GLib from .row import BoxflatRow from math import ceil, floor @@ -30,7 +28,7 @@ def __init__(self, title: str, subtitle="", max_value=1000, append_widget=True): self._present_cooldown = True - def _set_value(self, value: int) -> None: + def _set_value(self, value: int): if not self.get_active(): return @@ -40,25 +38,25 @@ def _set_value(self, value: int) -> None: if value < 0: value = 0 - GLib.idle_add(self._bar.set_value, value) + self._bar.set_value(value) - def set_bar_max(self, value: int) -> None: + def set_bar_max(self, value: int): self._max_value = value self._bar.set_max_value(value) - def set_bar_width(self, width: int) -> None: + def set_bar_width(self, width: int): self._bar.set_size_request(width, 0) - def set_offset(self, value: int) -> None: + def set_offset(self, value: int): value = ceil((value / 100) * self._max_value) self._bar.add_offset_value("level-offset", value) def set_present(self, value, additional=0, - skip_cooldown=False, trigger_cooldown=True) -> None: + skip_cooldown=False, trigger_cooldown=True): if self._present_cooldown and not skip_cooldown: self._present_cooldown = False @@ -67,9 +65,9 @@ def set_present(self, value, additional=0, super().set_present(value, additional) - def set_active(self, value, offset=0) -> None: + def set_active(self, value, offset=0): if super().set_active(value, offset): - self.set_value(0) + GLib.idle_add(self._bar.set_value, 0) def get_value(self) -> int: @@ -87,5 +85,6 @@ def get_percent(self) -> int: def get_percent_floor(self) -> int: return floor(self.get_fraction() * 100) + def get_percent_ceil(self) -> int: return ceil(self.get_fraction() * 100) diff --git a/boxflat/widgets/new_color_picker_row.py b/boxflat/widgets/new_color_picker_row.py index d2940d1..693526b 100644 --- a/boxflat/widgets/new_color_picker_row.py +++ b/boxflat/widgets/new_color_picker_row.py @@ -1,11 +1,11 @@ -import gi -gi.require_version('Gtk', '4.0') -from gi.repository import Gtk, Gdk +from gi.repository import Gtk, Gdk, GLib from .row import BoxflatRow from threading import Thread, Event, Lock from time import sleep +from boxflat.subscription import EventDispatcher -MOZA_RPM_LEDS=10 +MOZA_RPM_LEDS = 10 +MOZA_RGB_BUTTONS = 10 def extract_rgb(rgba: Gdk.RGBA) -> list: rgb = rgba.to_string()[4:-1] @@ -13,21 +13,26 @@ def extract_rgb(rgba: Gdk.RGBA) -> list: return rgb -class BoxflatNewColorPickerRow(BoxflatRow): - def __init__(self, title: str, subtitle="", blinking=False): - super().__init__(title, subtitle) +class BoxflatNewColorPickerRow(EventDispatcher, BoxflatRow): + def __init__(self, title="", subtitle="", blinking=False): + BoxflatRow.__init__(self, title, subtitle) + EventDispatcher.__init__(self) + + for i in range(MOZA_RPM_LEDS): + self._register_event(f"color{i}") child = self.get_child() main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.set_child(main_box) - colors_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True, halign=Gtk.Align.CENTER) + colors_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True, halign=Gtk.Align.FILL) colors_box.set_margin_top(6) colors_box.set_margin_bottom(12) main_box.append(child) main_box.add_css_class("header") main_box.set_valign(Gtk.Align.CENTER) + main_box.set_halign(Gtk.Align.FILL) main_box.append(colors_box) self._dialog = Gtk.ColorDialog(with_alpha=False) @@ -38,13 +43,16 @@ def __init__(self, title: str, subtitle="", blinking=False): red = Gdk.RGBA() red.parse("rgb(230,60,60)") for i in range(MOZA_RPM_LEDS): - color = Gtk.ColorDialogButton(dialog=self._dialog, hexpand=True, halign=Gtk.Align.CENTER) + color = Gtk.ColorDialogButton(dialog=self._dialog, hexpand=True) color.set_rgba(red) - color.set_size_request(0,48) + color.set_valign(Gtk.Align.CENTER) + color.set_halign(Gtk.Align.CENTER) color.connect('notify::rgba', self._notify) self._colors.append(color) colors_box.append(color) + size = color.get_preferred_size()[1] + color.set_size_request(0, size.width - 2) if not blinking: continue @@ -67,7 +75,7 @@ def get_index(self, button: Gtk.ColorDialogButton) -> int: return self._colors.index(button) - def set_led_value(self, value: list, index: int) -> None: + def set_led_value(self, value: list, index: int): if self._value_lock.locked(): return @@ -78,26 +86,28 @@ def set_led_value(self, value: list, index: int) -> None: # print("Still cooling down") return - # TODO: use Lock instead of boolean value - self._mute = True rgba = Gdk.RGBA() rgba.parse(f"rgb({value[0]},{value[1]},{value[2]})") + GLib.idle_add(self._set_led_value, rgba, index) + + def _set_led_value(self, rgba, index): + self._mute.set() self._colors[index].set_rgba(rgba) - self._mute = False + self._mute.clear() - def _notify(self, button: Gtk.ColorDialogButton, *param, alt_value=None) -> None: - if self._mute: + def _notify(self, button: Gtk.ColorDialogButton, *param, alt_value=None): + if self._mute.is_set(): return if self._cooldown == 0: self._cooldown = 1 + index = self.get_index(button) value = alt_value if alt_value else self.get_value(index) - for sub in self._subscribers: - sub[0](value, sub[2][0] + str(index+1)) + self._dispatch(f"color{index}", value) def _enter_button(self, controller: Gtk.EventControllerMotion, a, b, index: int): @@ -123,7 +133,7 @@ def _button_blinking(self, index: int): self._notify(button, alt_value=[0, 0, 0]) sleep(0.4) self._notify(button, alt_value=value) - sleep(0.8) + sleep(0.4) self._blinking_event[index].clear() - self._cooldown = 10 + self._cooldown = 8 diff --git a/boxflat/widgets/preferences_group.py b/boxflat/widgets/preferences_group.py index cdea3bf..dce29f0 100644 --- a/boxflat/widgets/preferences_group.py +++ b/boxflat/widgets/preferences_group.py @@ -8,7 +8,6 @@ class BoxflatPreferencesGroup(Adw.PreferencesGroup): def __init__(self, title="", level_bar=False, alt_level_bar=False): super().__init__() - self._subscribers = [] self._bar = None self._bar1 = None self._bar2 = None @@ -94,14 +93,14 @@ def get_bar_level(self) -> int: return int(self._bar.get_level()) - def set_bar_width(self, width: int) -> None: + def set_bar_width(self, width: int): if self._box: self._box.set_size_request(width/2, 0) elif self._bar: self._bar.set_size_request(width, 0) - def set_bar_max(self, value: int) -> None: + def set_bar_max(self, value: int): self._max_value = value if self._box: @@ -111,23 +110,15 @@ def set_bar_max(self, value: int) -> None: self._bar.set_max_value(value) - def set_offset(self, value: int) -> None: + def set_offset(self, value: int): self._offset = value - # def set_range_start(self, value: int) -> None: - # self._bar.set_min_value(round(self._max_value * (value/100))) - - - # def set_range_end(self, value: int) -> None: - # self._bar.set_max_value(round(self._max_value * (value/100))) - - - def set_active(self, value, offset=0) -> None: + def set_active(self, value, offset=0): value = int(value + offset) > 0 GLib.idle_add(self.set_sensitive, value) - def set_present(self, value, offset=0) -> None: + def set_present(self, value, offset=0): value = int(value) + offset > 0 GLib.idle_add(self.set_visible, value) diff --git a/boxflat/widgets/preset_dialog.py b/boxflat/widgets/preset_dialog.py new file mode 100644 index 0000000..527ea78 --- /dev/null +++ b/boxflat/widgets/preset_dialog.py @@ -0,0 +1,128 @@ +from gi.repository import Gtk, Adw, GLib + +from .button_row import BoxflatButtonRow +from .switch_row import BoxflatSwitchRow +from .advance_row import BoxflatAdvanceRow + +from boxflat.subscription import EventDispatcher + +class BoxflatPresetDialog(Adw.Dialog, EventDispatcher): + def __init__(self, presets_path: str, file_name: str): + Adw.Dialog.__init__(self) + EventDispatcher.__init__(self) + + self._register_events("save", "delete") + preset_name = file_name.removesuffix(".yml") + self._preset_name = preset_name + self.set_title("Preset settings") + self.set_content_width(480) + + # Create UI + self._name_row = Adw.EntryRow(title="Preset name") + self._name_row.set_text(preset_name) + + self._auto_apply = BoxflatSwitchRow("Apply automatically") + self._auto_apply.set_subtitle("Apply when selected process is running") + + self._auto_apply_name = Adw.EntryRow(title="Process name") + self._auto_apply_name.set_sensitive(False) + self._auto_apply.subscribe(self._auto_apply_name.set_sensitive) + + self._auto_apply_select = BoxflatAdvanceRow("Select running process") + self._auto_apply_select.set_active(False) + self._auto_apply_select.subscribe(self._open_process_page) + self._auto_apply.subscribe(self._auto_apply_select.set_active) + + # Place rows in logical order + page = Adw.PreferencesPage() + group = Adw.PreferencesGroup(margin_start=10, margin_end=10) + group.add(self._name_row) + page.add(group) + + group = Adw.PreferencesGroup(margin_start=10, margin_end=10) + group.add(self._auto_apply) + group.add(self._auto_apply_name) + group.add(self._auto_apply_select) + page.add(group) + + # Finally, add all things together + nav = Adw.NavigationView() + self.set_child(nav) + self._navigation = nav + + toolbar_view = Adw.ToolbarView() + nav.add(Adw.NavigationPage(title=f"\"{preset_name}\" preset settings", child=toolbar_view)) + + toolbar_view.add_top_bar(Adw.HeaderBar()) + toolbar_view.set_content(page) + + self._read_preset_data(presets_path, file_name) + + self._save_row = None + self._delete_row = None + # Decide which button style to use + if Adw.get_minor_version() >= 6: + self._save_row = Adw.ButtonRow(title="Save") + self._save_row.add_css_class("suggested-action") + self._save_row.set_end_icon_name("document-save-symbolic") + self._save_row.connect("activated", self._notify_save) + self._name_row.connect("notify::text-length", lambda e, *args: self._save_row.set_sensitive(e.get_text_length())) + + self._delete_row = Adw.ButtonRow(title="Delete") + self._delete_row.add_css_class("destructive-action") + self._delete_row.set_end_icon_name("user-trash-symbolic") + self._delete_row.connect("activated", self._notify_delete) + + group = Adw.PreferencesGroup(margin_start=100, margin_end=100) + group.add(self._delete_row) + page.add(group) + + group = Adw.PreferencesGroup(margin_start=100, margin_end=100) + group.add(self._save_row) + page.add(group) + + + # compatibility with libadwaita older than 1.6 + else: + self._save_row = Gtk.Button(label="Save", hexpand=True) + self._save_row.add_css_class("suggested-action") + self._save_row.add_css_class("square") + self._save_row.connect("clicked", self._notify_save) + self._name_row.connect("notify::text-length", lambda e, *args: self._save_row.set_sensitive(e.get_text_length())) + + self._delete_row = Gtk.Button(label="Delete", hexpand=True) + self._delete_row.add_css_class("destructive-action") + self._delete_row.add_css_class("square") + self._delete_row.connect("clicked", self._notify_delete) + + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + box.append(self._delete_row) + box.append(self._save_row) + box.add_css_class("linked") + + self.get_child().add_bottom_bar(box) + + + def _read_preset_data(self, presets_path: str, file_name: str): + print(f"Reading preset data from \"{file_name}\"") + # self.set_title(preset_name) + + + def _notify_save(self, *rest): + self.close() + self._dispatch("save", self._preset_name) + + + def _notify_delete(self, *rest): + self.close() + self._dispatch("delete", self._preset_name) + + + def _open_process_page(self, *rest): + page = Adw.PreferencesPage() + + toolbar_view = Adw.ToolbarView() + toolbar_view.add_top_bar(Adw.HeaderBar()) + toolbar_view.set_content(page) + + self._navigation.push(Adw.NavigationPage(title="Find game process", child=toolbar_view)) diff --git a/boxflat/widgets/row.py b/boxflat/widgets/row.py index 9cc4235..40d9dbb 100644 --- a/boxflat/widgets/row.py +++ b/boxflat/widgets/row.py @@ -1,47 +1,52 @@ -import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, GLib import time +from threading import Event +from boxflat.subscription import SimpleEventDispatcher -class BoxflatRow(Adw.ActionRow): +class BoxflatRow(Adw.ActionRow, SimpleEventDispatcher): def __init__(self, title="", subtitle=""): - super().__init__() + Adw.ActionRow.__init__(self) + SimpleEventDispatcher.__init__(self) + self._cooldown = 0 - self._subscribers = [] - self._mute = False + self._mute = Event() self._shutdown = False self.set_sensitive(True) self.set_title(title) self.set_subtitle(subtitle) self._expression = "*1" self._reverse_expression = "*1" - self.set_size_request(620, 0) + self._active = True + self._cooldown_increment = 1 def get_active(self) -> bool: - return self.get_sensitive() + return self._active def set_active(self, value=1, offset=0) -> bool: value = bool(int(value) + offset > 0) if value != self.get_active(): + self._active = value GLib.idle_add(self.set_sensitive, value) return True return False - def set_present(self, value, additional=0) -> None: + def set_present(self, value, additional=0): GLib.idle_add(self.set_visible, int(value) + additional > 0) - def mute(self, value: bool=True) -> None: - self._mute = value + def mute(self, value: bool=True): + if value: + self._mute.set() + else: + self.unmute() - def unmute(self) -> None: - self._mute = False + def unmute(self): + self._mute.clear() def get_value(self) -> int: @@ -52,62 +57,54 @@ def get_raw_value(self) -> int: return self.get_value() - def set_value(self, value, mute: bool=True) -> None: + def set_value(self, value, mute: bool=True): if self.cooldown(): # print("Still cooling down") - # print(self.get_title()) return + GLib.idle_add(self.__set_value_helper, value, mute) + - self.mute(mute) + def __set_value_helper(self, value, mute: bool=True): + self._mute.set() self._set_value(value) - self.unmute() + self._mute.clear() - def _set_value(self, value) -> None: + def _set_value(self, value): pass - def _set_widget(self, widget: Gtk.Widget) -> None: - GLib.idle_add(self.add_suffix, widget) - - - def subscribe(self, callback: callable, *args, raw=False) -> None: - self._subscribers.append((callback, raw, args)) - - - def clear_subscribtions(self) -> None: - self._subscribers = [] + def _set_widget(self, widget: Gtk.Widget): + self.add_suffix(widget) - def _notify(self) -> None: - if self._mute: + def _notify(self, *rest): + if self._mute.is_set(): return - self._cooldown = 1 - for sub in self._subscribers: - value = self.get_raw_value() if sub[1] else self.get_value() - sub[0](value, *sub[2]) + self._cooldown = self._cooldown_increment + self._dispatch(self.get_value()) - def set_expression(self, expr: str) -> None: + def set_expression(self, expr: str): """ Modify the value when invoking get_value() """ self._expression = expr - def set_reverse_expression(self, expr: str) -> None: + def set_reverse_expression(self, expr: str): """ Modify the value when invoking set_value() """ self._reverse_expression = expr - def shutdown(self) -> None: + def shutdown(self): self._shutdown = True - def set_width(self, width: int) -> None: + def set_width(self, width: int): self.set_size_request(width, 0) diff --git a/boxflat/widgets/slider_row.py b/boxflat/widgets/slider_row.py index 088b7d3..46ddca1 100644 --- a/boxflat/widgets/slider_row.py +++ b/boxflat/widgets/slider_row.py @@ -48,7 +48,7 @@ def __init__(self, title="", range_start=0, slider.add_css_class("slider") - # def _slider_increment_handler(self, scale) -> None: + # def _slider_increment_handler(self, scale): # modulo = self.get_value() % self._increment # if modulo != 0: @@ -57,17 +57,16 @@ def __init__(self, title="", range_start=0, # self._notify() - def _notify(self, *args) -> None: - super()._notify() + def add_marks(self, *marks: int): + for mark in marks: + self.add_mark(mark, str(mark)) - def add_marks(self, *marks: int) -> None: - for mark in marks: - self._slider.add_mark( - mark, Gtk.PositionType.BOTTOM,f"{mark}{self._suffix}") + def add_mark(self, value: int, text: str): + self._slider.add_mark(value, Gtk.PositionType.BOTTOM, f"{text}{self._suffix}") - def set_width(self, width: int) -> None: + def set_slider_width(self, width: int): self._slider.set_size_request(width, 0) @@ -79,7 +78,7 @@ def get_raw_value(self) -> int: return int(self._slider.get_value()) - def _set_value(self, value: int) -> None: + def _set_value(self, value: int): value = round(eval("value"+self._reverse_expression)) if value < self._range_start: value = self._range_start diff --git a/boxflat/widgets/switch_row.py b/boxflat/widgets/switch_row.py index 41ea871..17c7c73 100644 --- a/boxflat/widgets/switch_row.py +++ b/boxflat/widgets/switch_row.py @@ -9,13 +9,14 @@ def __init__(self, title: str, subtitle=""): switch = Gtk.Switch() switch.add_css_class("switch") - switch.connect('notify::active', lambda switch, whatever: self._notify()) + switch.connect('notify::active', self._notify) switch.set_valign(Gtk.Align.CENTER) self._reverse = False self._switch = switch self._set_widget(switch) self.set_activatable_widget(switch) self._value_store = None + self._cooldown_increment = 2 def get_value(self) -> int: @@ -29,11 +30,11 @@ def get_value(self) -> int: return round(eval("int(val)" + self._expression)) - def reverse_values(self) -> None: + def reverse_values(self): self._reverse = True - def _set_value(self, value: int) -> None: + def _set_value(self, value: int): if value < 0: return @@ -46,13 +47,14 @@ def _set_value(self, value: int) -> None: def set_active(self, value=1, offset=0, off_when_inactive=False) -> bool: + tmp = self.get_value() change = super().set_active(value, offset=offset) if not change: return change if off_when_inactive and self._value_store == None: - self._value_store = self.get_value() + self._value_store = tmp self.set_value(0) elif off_when_inactive and self._value_store != None: diff --git a/boxflat/widgets/toggle_button_row.py b/boxflat/widgets/toggle_button_row.py index d28bd8b..c98b579 100644 --- a/boxflat/widgets/toggle_button_row.py +++ b/boxflat/widgets/toggle_button_row.py @@ -15,7 +15,7 @@ def __init__(self, title: str, subtitle=""): self._box.add_css_class("linked") - def add_buttons(self, *labels: str) -> None: + def add_buttons(self, *labels: str): for label in labels: button = Gtk.ToggleButton(label=label) button.set_valign(Gtk.Align.CENTER) @@ -39,7 +39,7 @@ def get_value(self) -> int: return round(eval("val" + self._expression)) - def _set_value(self, value: int) -> None: + def _set_value(self, value: int): if value == -1: for button in self._buttons: button.set_active(False) @@ -55,7 +55,7 @@ def _set_value(self, value: int) -> None: self._buttons[value].set_active(True) - def _notify(self, button: Gtk.ToggleButton) -> None: + def _notify(self, button: Gtk.ToggleButton): if button.get_active(): super()._notify() diff --git a/data/serial.yml b/data/serial.yml index c0def2e..74316d2 100644 --- a/data/serial.yml +++ b/data/serial.yml @@ -659,7 +659,7 @@ commands: # Steering wheel wheel-colors: - read: 64 + read: -1 # 64 write: 63 id: [0] bytes: 15 @@ -1161,31 +1161,6 @@ commands: bytes: 2 type: int - # Even PitHouse is not sending anything - # These options are dead - # They didn't work correctly and were probably disabled - # some time in 2023 - hpattern-throttle-blip: - read: -1 - write: -1 - id: [-1] - bytes: 2 - type: int - - hpattern-blip-output: - read: -1 - write: -1 - id: [-1] - bytes: 2 - type: int - - hpattern-blip-duration: - read: -1 - write: -1 - id: [-1] - bytes: 2 - type: int - # estop estop-set-status: read: -1 @@ -1464,4 +1439,4 @@ commands: write: 65 id: [253, 222] bytes: 4 - type: array + type: int diff --git a/data/style.css b/data/style.css index b8bf71d..e3a1132 100644 --- a/data/style.css +++ b/data/style.css @@ -115,7 +115,7 @@ levelbar { } colorbutton { - margin-right: 13px; + margin-right: 0px; margin-top: 0; } @@ -154,7 +154,8 @@ colorbutton > button > colorswatch { background-color: #b8af38; } -.level-button { - padding-left: 10px; - padding-right: 10px; +.square { + border-radius: 0; + padding-top: 10px; + padding-bottom: 10px; } diff --git a/data/version b/data/version index b597841..40a06c5 100644 --- a/data/version +++ b/data/version @@ -1 +1 @@ -v1.18.5 +v1.19.0 diff --git a/entrypoint.py b/entrypoint.py index 092b68f..940c284 100755 --- a/entrypoint.py +++ b/entrypoint.py @@ -8,6 +8,7 @@ parser.add_argument("--dry-run", help="Don't send any data to the serial devices", action="store_true", required=False) parser.add_argument("--data-path", help="Use arbitrary data path", type=str, required=False) parser.add_argument("--flatpak", help="for flatpak usage", action="store_true", required=False) +parser.add_argument("--custom", help="Enable custom commands entry", action="store_true", required=False) args = parser.parse_args() data_path = "/usr/share/boxflat/data" @@ -23,5 +24,5 @@ if args.flatpak: data_path = "/app/share/boxflat/data" -app = app.MyApp(data_path, config_path, args.dry_run, application_id="io.github.lawstorant.boxflat") +app = app.MyApp(data_path, config_path, args.dry_run, args.custom, application_id="io.github.lawstorant.boxflat") app.run() diff --git a/io.github.lawstorant.boxflat.metainfo.xml b/io.github.lawstorant.boxflat.metainfo.xml index e363cc6..fd59b26 100644 --- a/io.github.lawstorant.boxflat.metainfo.xml +++ b/io.github.lawstorant.boxflat.metainfo.xml @@ -94,6 +94,24 @@ forest10pl@gmail.com + + +

Major rewrite pt.1. No user-facing changes :/ Maybe less crashing

+
    +
  • Unified event handling through new classes Subscription, SubscriptionList, EventDispatcher, Observable etc.
  • +
  • Make sure everything is updated through idle_add
  • +
  • Cutting spaghetti from connection manager. No more per-type setters/getters
  • +
  • Consume command queue in real time to enable faster writes to devices
  • +
  • MozaCommands now handles type conversion automatically
  • +
  • Use more Events/Locks instead of boolean values
  • +
  • Create Threads on demand instead of keeping them alive with loops and sleep()
  • +
  • Remove all "None" return type hints
  • +
  • RPM/Button color picker row is now sized more universally
  • +
  • Custom commands are now accessible with "--custom" launch flag
  • +
  • Instant curve preset application
  • +
+
+
    @@ -156,7 +174,7 @@
    • Preset saving/loading
    • Remove some lingering text strings from preset rows
    • -
    • Fix crashed related to reloading presets view
    • +
    • Fix crashes related to reloading presets view