diff --git a/dist/EX-Installer-Linux64 b/dist/EX-Installer-Linux64 index 21c8ba7..f777a67 100755 Binary files a/dist/EX-Installer-Linux64 and b/dist/EX-Installer-Linux64 differ diff --git a/dist/EX-Installer-Win32.exe b/dist/EX-Installer-Win32.exe index 6b9973a..df7d84a 100644 Binary files a/dist/EX-Installer-Win32.exe and b/dist/EX-Installer-Win32.exe differ diff --git a/dist/EX-Installer-Win64.exe b/dist/EX-Installer-Win64.exe index 88c9947..db4b04b 100644 Binary files a/dist/EX-Installer-Win64.exe and b/dist/EX-Installer-Win64.exe differ diff --git a/dist/EX-Installer-macOS b/dist/EX-Installer-macOS index 13e0747..df8bbc4 100755 Binary files a/dist/EX-Installer-macOS and b/dist/EX-Installer-macOS differ diff --git a/ex_installer/__main__.py b/ex_installer/__main__.py index ec57fde..6a66d67 100644 --- a/ex_installer/__main__.py +++ b/ex_installer/__main__.py @@ -108,6 +108,6 @@ def main(debug): debug = True else: debug = False - + # Start the app main(debug) diff --git a/ex_installer/common_fonts.py b/ex_installer/common_fonts.py index 6e8c98c..a1b8a11 100644 --- a/ex_installer/common_fonts.py +++ b/ex_installer/common_fonts.py @@ -47,6 +47,11 @@ def __init__(self, root): size=14, weight="bold") + self.italic_instruction_font = ctk.CTkFont(family=self.default_font, + size=14, + weight="normal", + slant="italic") + self.large_bold_instruction_font = ctk.CTkFont(family=self.default_font, size=16, weight="bold") diff --git a/ex_installer/common_widgets.py b/ex_installer/common_widgets.py index 052bfaa..d148618 100644 --- a/ex_installer/common_widgets.py +++ b/ex_installer/common_widgets.py @@ -85,6 +85,7 @@ def __init__(self, parent, *args, **kwargs): # Define fonts self.instruction_font = self.common_fonts.instruction_font self.bold_instruction_font = self.common_fonts.bold_instruction_font + self.italic_instruction_font = self.common_fonts.italic_instruction_font self.large_bold_instruction_font = self.common_fonts.large_bold_instruction_font self.small_italic_instruction_font = self.common_fonts.small_italic_instruction_font self.title_font = self.common_fonts.title_font diff --git a/ex_installer/compile_upload.py b/ex_installer/compile_upload.py index f339f60..9b484dc 100644 --- a/ex_installer/compile_upload.py +++ b/ex_installer/compile_upload.py @@ -241,6 +241,13 @@ def show_backup_popup(self): self.backup_popup.focus() self.backup_popup.lift(self) + # Ensure pop up is within the confines of the app window to start + main_window = self.winfo_toplevel() + backup_offset_x = main_window.winfo_x() + 75 + backup_offset_y = main_window.winfo_y() + 200 + self.backup_popup.geometry(f"+{backup_offset_x}+{backup_offset_y}") + self.backup_popup.update() + # Set icon and title if sys.platform.startswith("win"): self.backup_popup.after(250, lambda icon=images.DCC_EX_ICON_ICO: self.backup_popup.iconbitmap(icon)) @@ -337,7 +344,7 @@ def backup_config_files(self, overwrite): self.backup_button.grid() else: if self.backup_path.get() == "": - message = "You must specific a valid folder to backup to" + message = "You must specify a valid folder to backup to" else: message = f"{self.backup_path.get()} is not a valid directory" self.status_text.configure(text=message, diff --git a/ex_installer/ex_commandstation.py b/ex_installer/ex_commandstation.py index f5fec0d..90273fb 100644 --- a/ex_installer/ex_commandstation.py +++ b/ex_installer/ex_commandstation.py @@ -744,9 +744,6 @@ def generate_config(self): else: line = '#define WIFI_SSID "' + self.wifi_ssid_entry.get() + '"\n' config_list.append(line) - # if self.wifi_pwd_entry.get() == "": - # param_errors.append("WiFi password not set") - # else: invalid, issue = self.check_invalid_wifi_password() if invalid: param_errors.append(issue) diff --git a/ex_installer/ex_installer.py b/ex_installer/ex_installer.py index 60a7bab..ce83d97 100644 --- a/ex_installer/ex_installer.py +++ b/ex_installer/ex_installer.py @@ -120,6 +120,7 @@ def __init__(self, *args, **kwargs): self.info_menu.add_command(label="About", command=self.about) self.info_menu.add_command(label="DCC-EX Website", command=self.website) self.info_menu.add_command(label="EX-Installer Instructions", command=self.instructions) + self.info_menu.add_command(label="EX-Installer News", command=self.news) self.enable_debug = ctk.StringVar(self, value="off") self.info_menu.add_checkbutton(label="Enable debug logging", command=self.toggle_debug, variable=self.enable_debug, onvalue="on", offvalue="off") @@ -275,6 +276,12 @@ def instructions(self): """ webbrowser.open_new("https://dcc-ex.com/ex-installer/index.html") + def news(self): + """ + Link to DCC-EX News articles for EX-Installer + """ + webbrowser.open_new("https://dcc-ex.com/news/tag/ex-installer.html") + def toggle_debug(self): """ Function to enable/disable debug logging from the menu diff --git a/ex_installer/ex_turntable.py b/ex_installer/ex_turntable.py index 1b8127e..b61bee3 100644 --- a/ex_installer/ex_turntable.py +++ b/ex_installer/ex_turntable.py @@ -35,16 +35,6 @@ class EXTurntable(WindowLayout): Class for the EX-Turntable view """ - advanced_config_options = [ - "#define LED_FAST 100\n", - "#define LED_SLOW 500\n", - "// #define DEBUG\n", - "// #define SANITY_STEPS 10000\n", - "// #define HOME_SENSITIVITY 300\n", - "// #define FULL_STEP_COUNT 4096\n", - "// #define DEBOUNCE_DELAY 10\n" - ] - def __init__(self, parent, *args, **kwargs): """ Initialise view @@ -94,44 +84,87 @@ def set_product_version(self, version, major=None, minor=None, patch=None): function_disables_track_manager() """ self.product_version_name = version + self.get_steppers() if major is not None: self.product_major_version = major if minor is not None: self.product_minor_version = minor if patch is not None: self.product_patch_version = patch + # Disable pre 0.6.0 features + if self.product_major_version == 0 and self.product_minor_version < 6: + self.gearing_label.configure(state="disabled", font=self.italic_instruction_font) + self.gearing_entry.configure(state="disabled", font=self.italic_instruction_font) + self.gearing.set(1) + else: + self.gearing_label.configure(state="normal", font=self.instruction_font) + self.gearing_entry.configure(state="normal", font=self.instruction_font) + # Disable pre 0.7.0 features + if self.product_major_version == 0 and self.product_minor_version < 7: + self.invert_dir_switch.deselect() + self.invert_dir_switch.configure(state="disabled") + self.invert_step_switch.deselect() + self.invert_step_switch.configure(state="disabled") + self.invert_enable_switch.deselect() + self.invert_enable_switch.configure(state="disabled") + self.forward_only_switch.deselect() + self.forward_only_switch.configure(state="disabled") + self.reverse_only_switch.deselect() + self.reverse_only_switch.configure(state="disabled") + else: + self.invert_dir_switch.configure(state="normal") + self.invert_step_switch.configure(state="normal") + self.invert_enable_switch.configure(state="normal") + self.forward_only_switch.configure(state="normal") + self.reverse_only_switch.configure(state="normal") def setup_config_frame(self): """ Setup the container frame for configuration options Default config parameters from config.example.h: - - #define I2C_ADDRESS 0x60 - - #define TURNTABLE_EX_MODE TURNTABLE - - // #define TURNTABLE_EX_MODE TRAVERSER - - // #define SENSOR_TESTING - - #define HOME_SENSOR_ACTIVE_STATE LOW - - #define LIMIT_SENSOR_ACTIVE_STATE LOW - - #define RELAY_ACTIVE_STATE HIGH - - #define PHASE_SWITCHING AUTO - - #define PHASE_SWITCH_ANGLE 45 - - #define STEPPER_DRIVER ULN2003_HALF_CW (READ FROM standard_steppers.h) - - #define DISABLE_OUTPUTS_IDLE - - #define STEPPER_MAX_SPEED 200 - - #define STEPPER_ACCELERATION 25 + - #define I2C_ADDRESS 0x60 - General + - #define TURNTABLE_EX_MODE TURNTABLE - General + - // #define TURNTABLE_EX_MODE TRAVERSER - General + - // #define SENSOR_TESTING - General + - #define HOME_SENSOR_ACTIVE_STATE LOW - General + - #define LIMIT_SENSOR_ACTIVE_STATE LOW - General + - #define RELAY_ACTIVE_STATE HIGH - General + - #define PHASE_SWITCHING AUTO - General + - #define PHASE_SWITCH_ANGLE 45 - General + - #define STEPPER_DRIVER ULN2003_HALF_CW - Stepper + - #define DISABLE_OUTPUTS_IDLE - Stepper + - #define STEPPER_MAX_SPEED 200 - Stepper + - #define STEPPER_ACCELERATION 25 - Stepper + + New in 0.6.0: + - #define STEPPER_GEARING_FACTOR 1 - Stepper + + New in 0.7.0: + - // #define INVERT_DIRECTION - Stepper + - // #define INVERT_STEP - Stepper + - // #define INVERT_ENABLE - Stepper + - // #define ROTATE_FORWARD_ONLY - Stepper + - // #define ROTATE_REVERSE_ONLY - Stepper + + Advanced: + - #define LED_FAST 100 - Advanced + - #define LED_SLOW 500 - Advanced + - // #define DEBUG - Advanced + - // #define SANITY_STEPS 10000 - Advanced + - // #define HOME_SENSITIVITY 300 - Advanced + - // #define FULL_STEP_COUNT 4096 - Advanced + - // #define DEBOUNCE_DELAY 10 - Advanced """ grid_options = {"padx": 5, "pady": 5} - self.config_frame.grid_columnconfigure(0, weight=1) - self.config_frame.grid_rowconfigure((0, 1, 2, 3, 4, 5), weight=1) toggle_options = {"text": None, "fg_color": "#00A3B9", "progress_color": "#00A3B9", "width": 30, "bg_color": "#D9D9D9"} toggle_label_options = {"width": 80, "bg_color": "#D9D9D9"} subframe_options = {"border_width": 0} # Instructions - instructions = ("EX-Turntable requires a litte more DIY knowledge when it comes to working with stepper " + - "drivers and motors and the home and limit sensors. Please ensure you read the " + - "documentation prior to installing (Click this text to open it).") + instructions = ("You must select the appropriate options on the General and Stepper Options tab before " + + "you can continue. Please also ensure you read the documentation prior to installing.") # Tooltip text i2c_tip = ("You need to specify an available, valid I\u00B2C address for EX-Turntable. Valid values are " + @@ -162,97 +195,118 @@ def setup_config_frame(self): accel_tip = ("This defines the rate at which the stepper speed increases to the top speed, and decreases " + "until it stops.") advanced_tip = ("Enable this option to be able to directly edit the configuration file on the next screen.") - - # Create subframes for grouping - self.main_options_frame = ctk.CTkFrame(self.config_frame, width=760) # I2C, operating mode - self.stepper_frame = ctk.CTkFrame(self.config_frame, width=760) # Stepper driver, disable idle, speed/accel - self.phase_frame = ctk.CTkFrame(self.config_frame, width=760) # Auto, angle, relay active - self.sensor_frame = ctk.CTkFrame(self.config_frame, width=760) # Testing, home/limit - - # Instruction widgets + gear_factor_tip = ("For step counts larger than 32767 you must set an appropriate gearing factor. Click this " + + "tip to open the EX-Turntable documentation (From version 0.6.0).") + invert_direction_tip = ("Enable this to invert the direction pin for two wire stepper drivers. This does not " + + "to ULN2003 (from version 0.7.0).") + invert_step_tip = ("Enable this to invert the step pin for two wire stepper drivers. This does not " + + "to ULN2003 (from version 0.7.0).") + invert_enable_tip = ("Enable this to invert the enable pin for two wire stepper drivers. This does not " + + "to ULN2003. Enable this if you previously used the A4988_INV stepper option " + + "(from version 0.7.0).") + forward_only_tip = ("Enable this to force the stepper to rotate in the forward direction only. This can be " + + "useful to work around stepper or gearbox slop (from version 0.7.0).") + reverse_only_tip = ("Enable this to force the stepper to rotate in the reverse direction only. This can be " + + "useful to work around stepper or gearbox slop (from version 0.7.0).") + advanced_tip = ("Refer to the EX-Turntable documentation for information on advanced options. Click this tip " + + "to open the relevant documentation.") + + # Setup tabview for config options + self.config_tabview = ctk.CTkTabview(self.config_frame, border_width=2, + segmented_button_fg_color="#00A3B9", + segmented_button_unselected_color="#00A3B9", + segmented_button_selected_color="#00353D", + segmented_button_selected_hover_color="#017E8F", + text_color="white") + + tab_list = [ + "General", + "Stepper Options", + "Advanced" + ] + for tab in tab_list: + self.config_tabview.add(tab) + self.config_tabview.tab(tab).grid_columnconfigure(0, weight=1) + self.config_tabview.tab(tab).grid_rowconfigure(0, weight=1) + + # Tab frames + tab_frame_options = {"column": 0, "row": 0, "sticky": "nsew"} + self.general_tab_frame = ctk.CTkFrame(self.config_tabview.tab("General"), border_width=0) + self.general_tab_frame.grid(**tab_frame_options) + self.stepper_tab_frame = ctk.CTkFrame(self.config_tabview.tab("Stepper Options"), border_width=0) + self.stepper_tab_frame.grid(**tab_frame_options) + self.advanced_tab_frame = ctk.CTkFrame(self.config_tabview.tab("Advanced"), border_width=0) + self.advanced_tab_frame.grid(**tab_frame_options) + + # Instruction widget self.instruction_label = ctk.CTkLabel(self.config_frame, text=instructions, width=780, wraplength=760, font=self.instruction_font) self.instruction_label.bind("", lambda x: webbrowser.open_new("https://dcc-ex.com/ex-turntable/index.html")) + # Create general subframes for grouping + self.main_options_frame = ctk.CTkFrame(self.general_tab_frame, width=760, border_width=0) + self.phase_frame = ctk.CTkFrame(self.general_tab_frame, width=760, border_width=0) + self.sensor_frame = ctk.CTkFrame(self.general_tab_frame, width=760, border_width=0) + # Create I2C widgets self.i2c_address = ctk.StringVar(self, value=60) self.i2c_address_frame = ctk.CTkFrame(self.main_options_frame, border_width=0) self.i2c_address_label = ctk.CTkLabel(self.i2c_address_frame, text="Set I\u00B2C address:", font=self.instruction_font) CreateToolTip(self.i2c_address_label, i2c_tip, "https://dcc-ex.com/ex-turntable/configure.html#i2c-address") - self.i2c_address_minus = ctk.CTkButton(self.i2c_address_frame, text="-", width=30, + self.set_i2c_frame = ctk.CTkFrame(self.i2c_address_frame, border_width=2) + self.i2c_address_minus = ctk.CTkButton(self.set_i2c_frame, text="-", width=30, command=self.decrement_address) - self.i2c_entry_frame = ctk.CTkFrame(self.i2c_address_frame, border_width=2, border_color="#00A3B9") + self.i2c_entry_frame = ctk.CTkFrame(self.set_i2c_frame, border_width=0) self.i2c_0x_label = ctk.CTkLabel(self.i2c_entry_frame, text="0x", font=self.instruction_font, width=20, padx=0, pady=0) self.i2c_address_entry = ctk.CTkEntry(self.i2c_entry_frame, textvariable=self.i2c_address, - width=30, border_width=2, justify="left", + width=30, border_width=0, justify="left", font=self.instruction_font) - self.i2c_address_plus = ctk.CTkButton(self.i2c_address_frame, text="+", width=30, + self.i2c_address_plus = ctk.CTkButton(self.set_i2c_frame, text="+", width=30, command=self.increment_address) # Validate I2C address if entered manually self.i2c_address_entry.bind("", self.validate_i2c_address) # Layout I2C address frame - self.i2c_address_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.i2c_address_frame.grid_columnconfigure((0, 1), weight=1) self.i2c_address_frame.grid_rowconfigure(0, weight=1) self.i2c_entry_frame.grid_columnconfigure((0, 1), weight=1) self.i2c_entry_frame.grid_rowconfigure(0, weight=1) + self.set_i2c_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.set_i2c_frame.grid_rowconfigure(0, weight=1) + self.set_i2c_frame.grid(column=1, row=0, **grid_options) + self.i2c_0x_label.grid(column=0, row=0, sticky="e", pady=2) + self.i2c_address_entry.grid(column=1, row=0, padx=0, pady=2) self.i2c_address_label.grid(column=0, row=0, **grid_options) - self.i2c_address_minus.grid(column=1, row=0, padx=(5, 0)) - self.i2c_0x_label.grid(column=0, row=0, sticky="e") - self.i2c_address_entry.grid(column=1, row=0, padx=0) - self.i2c_entry_frame.grid(column=2, row=0, padx=0) - self.i2c_address_plus.grid(column=3, row=0, sticky="w", padx=(0, 5)) + self.i2c_address_minus.grid(column=1, row=0, padx=(4, 0), pady=2) + self.i2c_entry_frame.grid(column=2, row=0, pady=2) + self.i2c_address_plus.grid(column=3, row=0, sticky="w", padx=(0, 4), pady=2) # Create mode widgets self.mode_frame = ctk.CTkFrame(self.main_options_frame, **subframe_options) - self.mode_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.mode_frame.grid_columnconfigure((0, 1), weight=1) self.mode_frame.grid_rowconfigure(0, weight=1) self.mode_label = ctk.CTkLabel(self.mode_frame, text="Select the operating mode:", font=self.instruction_font) CreateToolTip(self.mode_label, mode_tip, "https://dcc-ex.com/ex-turntable/traverser.html") - self.turntable_label = ctk.CTkLabel(self.mode_frame, text="Turntable", **toggle_label_options) - self.mode_switch = ctk.CTkSwitch(self.mode_frame, onvalue="TRAVERSER", offvalue="TURNTABLE", + self.mode_switch_frame = ctk.CTkFrame(self.mode_frame, border_width=2) + self.mode_switch_frame.grid_columnconfigure((0, 1, 2), weight=1) + self.mode_switch_frame.grid_rowconfigure(0, weight=1) + self.turntable_label = ctk.CTkLabel(self.mode_switch_frame, text="Turntable", **toggle_label_options) + self.mode_switch = ctk.CTkSwitch(self.mode_switch_frame, onvalue="TRAVERSER", offvalue="TURNTABLE", command=self.set_mode, **toggle_options) - self.traverser_label = ctk.CTkLabel(self.mode_frame, text="Traverser", **toggle_label_options) + self.traverser_label = ctk.CTkLabel(self.mode_switch_frame, text="Traverser", **toggle_label_options) # Layout mode frame self.mode_label.grid(column=0, row=0, sticky="e", **grid_options) - self.turntable_label.grid(column=1, row=0, sticky="nse", padx=(5, 0), pady=5) - self.mode_switch.grid(column=2, row=0, sticky="nsew", padx=0, pady=5) - self.traverser_label.grid(column=3, row=0, sticky="nsw", padx=(0, 5), pady=5) - - # Create stepper selection widgets - self.stepper_label = ctk.CTkLabel(self.stepper_frame, text="Select the stepper driver:", - font=self.instruction_font) - CreateToolTip(self.stepper_label, stepper_tip, - "https://dcc-ex.com/ex-turntable/purchasing.html#supported-stepper-drivers-and-motors") - self.stepper_combo = ctk.CTkComboBox(self.stepper_frame, values=["Select stepper driver"], - width=200, command=self.check_stepper) - - # Create disable stepper widget - self.disable_idle_switch = ctk.CTkSwitch(self.stepper_frame, text="Disable stepper when idle", - onvalue="on", offvalue="off", font=self.instruction_font) - self.disable_idle_switch.select() - CreateToolTip(self.disable_idle_switch, idle_tip) - - # Create stepper tuning widgets - self.speed_label = ctk.CTkLabel(self.stepper_frame, text="Set the stepper top speed", - font=self.instruction_font) - CreateToolTip(self.speed_label, speed_tip) - self.speed = ctk.StringVar(self, value="200") - self.speed_entry = ctk.CTkEntry(self.stepper_frame, textvariable=self.speed, font=self.instruction_font, - width=60) - self.accel_label = ctk.CTkLabel(self.stepper_frame, text="Set acceleration rate", - font=self.instruction_font) - CreateToolTip(self.accel_label, accel_tip) - self.accel = ctk.StringVar(self, value="25") - self.accel_entry = ctk.CTkEntry(self.stepper_frame, textvariable=self.accel, font=self.instruction_font, - width=40) + self.mode_switch_frame.grid(column=1, row=0, **grid_options) + self.turntable_label.grid(column=0, row=0, sticky="nse", padx=(5, 0), pady=5) + self.mode_switch.grid(column=1, row=0, sticky="nsew", padx=0, pady=5) + self.traverser_label.grid(column=2, row=0, sticky="nsw", padx=(0, 5), pady=5) # Create phase switch widgets self.auto_switch = ctk.CTkSwitch(self.phase_frame, text="Automatic phase switch at angle:", @@ -267,29 +321,33 @@ def setup_config_frame(self): # Create relay widgets self.relay_frame = ctk.CTkFrame(self.phase_frame, **subframe_options) - self.relay_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.relay_frame.grid_columnconfigure((0, 1), weight=1) self.relay_frame.grid_rowconfigure(0, weight=1) self.relay_label = ctk.CTkLabel(self.relay_frame, text="Relay type:", font=self.instruction_font) CreateToolTip(self.relay_label, relay_tip) - self.relay_low_label = ctk.CTkLabel(self.relay_frame, text="Active Low", **toggle_label_options) - self.relay_switch = ctk.CTkSwitch(self.relay_frame, onvalue="HIGH", offvalue="LOW", + self.relay_switch_frame = ctk.CTkFrame(self.relay_frame, border_width=2) + self.relay_switch_frame.grid_columnconfigure((0, 1, 2), weight=1) + self.relay_switch_frame.grid_rowconfigure(0, weight=1) + self.relay_low_label = ctk.CTkLabel(self.relay_switch_frame, text="Active Low", **toggle_label_options) + self.relay_switch = ctk.CTkSwitch(self.relay_switch_frame, onvalue="HIGH", offvalue="LOW", command=self.set_relay, **toggle_options) self.relay_switch.select() - self.relay_high_label = ctk.CTkLabel(self.relay_frame, text="Active High", **toggle_label_options) + self.relay_high_label = ctk.CTkLabel(self.relay_switch_frame, text="Active High", **toggle_label_options) # Layout relay frame self.relay_label.grid(column=0, row=0, sticky="e", padx=(5, 1), pady=5) - self.relay_low_label.grid(column=1, row=0, sticky="nse", padx=(5, 0), pady=5) - self.relay_switch.grid(column=2, row=0, sticky="nsew", padx=0, pady=5) - self.relay_high_label.grid(column=3, row=0, sticky="nsw", padx=(0, 5), pady=5) + self.relay_switch_frame.grid(column=1, row=0, **grid_options) + self.relay_low_label.grid(column=0, row=0, sticky="nse", padx=(5, 0), pady=5) + self.relay_switch.grid(column=1, row=0, sticky="nsew", padx=0, pady=5) + self.relay_high_label.grid(column=2, row=0, sticky="nsw", padx=(0, 5), pady=5) # Create sensor widgets self.home_sensor_frame = ctk.CTkFrame(self.sensor_frame, **subframe_options) self.limit_sensor_frame = ctk.CTkFrame(self.sensor_frame, **subframe_options) - self.home_sensor_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.home_sensor_frame.grid_columnconfigure((0, 1), weight=1) self.home_sensor_frame.grid_rowconfigure(0, weight=1) - self.limit_sensor_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.limit_sensor_frame.grid_columnconfigure((0, 1), weight=1) self.limit_sensor_frame.grid_rowconfigure(0, weight=1) self.home_label = ctk.CTkLabel(self.home_sensor_frame, text="Home sensor type:", font=self.instruction_font) @@ -297,33 +355,41 @@ def setup_config_frame(self): self.limit_label = ctk.CTkLabel(self.limit_sensor_frame, text="Limit sensor type:", font=self.instruction_font) CreateToolTip(self.limit_label, limit_tip) - self.home_low_label = ctk.CTkLabel(self.home_sensor_frame, text="Active Low", **toggle_label_options) - self.home_switch = ctk.CTkSwitch(self.home_sensor_frame, onvalue="HIGH", offvalue="LOW", + self.home_switch_frame = ctk.CTkFrame(self.home_sensor_frame, border_width=2) + self.home_switch_frame.grid_columnconfigure((0, 1, 2), weight=1) + self.home_switch_frame.grid_rowconfigure(0, weight=1) + self.home_low_label = ctk.CTkLabel(self.home_switch_frame, text="Active Low", **toggle_label_options) + self.home_switch = ctk.CTkSwitch(self.home_switch_frame, onvalue="HIGH", offvalue="LOW", command=self.set_home, **toggle_options) - self.home_high_label = ctk.CTkLabel(self.home_sensor_frame, text="Active High", **toggle_label_options) - self.limit_low_label = ctk.CTkLabel(self.limit_sensor_frame, text="Active Low", **toggle_label_options) - self.limit_switch = ctk.CTkSwitch(self.limit_sensor_frame, onvalue="HIGH", offvalue="LOW", + self.home_high_label = ctk.CTkLabel(self.home_switch_frame, text="Active High", **toggle_label_options) + self.limit_switch_frame = ctk.CTkFrame(self.limit_sensor_frame, border_width=2) + self.limit_switch_frame.grid_columnconfigure((0, 1, 2), weight=1) + self.limit_switch_frame.grid_rowconfigure(0, weight=1) + self.limit_low_label = ctk.CTkLabel(self.limit_switch_frame, text="Active Low", **toggle_label_options) + self.limit_switch = ctk.CTkSwitch(self.limit_switch_frame, onvalue="HIGH", offvalue="LOW", command=self.set_limit, **toggle_options) - self.limit_high_label = ctk.CTkLabel(self.limit_sensor_frame, text="Active High", **toggle_label_options) + self.limit_high_label = ctk.CTkLabel(self.limit_switch_frame, text="Active High", **toggle_label_options) # Layout sensor frames self.home_label.grid(column=0, row=0, sticky="e", **grid_options) - self.home_low_label.grid(column=1, row=0, sticky="nse", padx=(5, 0), pady=5) - self.home_switch.grid(column=2, row=0, sticky="nsew", padx=0, pady=5) - self.home_high_label.grid(column=3, row=0, sticky="nww", padx=(0, 5), pady=5) + self.home_switch_frame.grid(column=1, row=0, **grid_options) + self.home_low_label.grid(column=0, row=0, sticky="nse", padx=(5, 0), pady=5) + self.home_switch.grid(column=1, row=0, sticky="nsew", padx=0, pady=5) + self.home_high_label.grid(column=2, row=0, sticky="nww", padx=(0, 5), pady=5) self.limit_label.grid(column=0, row=0, sticky="e", **grid_options) - self.limit_low_label.grid(column=1, row=0, sticky="nse", padx=(5, 0), pady=5) - self.limit_switch.grid(column=2, row=0, sticky="nsew", padx=0, pady=5) - self.limit_high_label.grid(column=3, row=0, sticky="nsw", padx=(0, 5), pady=5) + self.limit_switch_frame.grid(column=1, row=0, **grid_options) + self.limit_low_label.grid(column=0, row=0, sticky="nse", padx=(5, 0), pady=5) + self.limit_switch.grid(column=1, row=0, sticky="nsew", padx=0, pady=5) + self.limit_high_label.grid(column=2, row=0, sticky="nsw", padx=(0, 5), pady=5) - # Create test and stepper disable idle widgets + # Create sensor test widget self.sensor_test_switch = ctk.CTkSwitch(self.sensor_frame, text="Enable sensor testing mode", onvalue="on", offvalue="off", font=self.instruction_font) CreateToolTip(self.sensor_test_switch, sensor_test_tip, "https://dcc-ex.com/ex-turntable/traverser.html#considerations-turntable-vs-traverser") - # Advanced config widget - self.advanced_config_enabled = ctk.CTkSwitch(self.config_frame, text="Advanced Config", + # Edit config widget + self.advanced_config_enabled = ctk.CTkSwitch(self.config_frame, text="Edit Config", onvalue="on", offvalue="off", font=self.instruction_font, command=self.set_advanced_config) @@ -332,19 +398,8 @@ def setup_config_frame(self): # Layout main options frame self.main_options_frame.grid_columnconfigure((0, 1), weight=1) self.main_options_frame.grid_rowconfigure(0, weight=1) - self.i2c_address_frame.grid(column=0, row=0, padx=(5, 0), pady=5) - self.mode_frame.grid(column=1, row=0, padx=(0, 5), pady=5) - - # Layout stepper frame - self.stepper_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) - self.stepper_frame.grid_rowconfigure((0, 1), weight=1) - self.stepper_label.grid(column=0, row=0, sticky="e", **grid_options) - self.stepper_combo.grid(column=1, row=0, columnspan=2, sticky="w", **grid_options) - self.disable_idle_switch.grid(column=3, row=0, **grid_options) - self.speed_label.grid(column=0, row=1, sticky="e", **grid_options) - self.speed_entry.grid(column=1, row=1, sticky="w", **grid_options) - self.accel_label.grid(column=2, row=1, sticky="e", **grid_options) - self.accel_entry.grid(column=3, row=1, sticky="w", **grid_options) + self.i2c_address_frame.grid(column=0, row=0, **grid_options) + self.mode_frame.grid(column=1, row=0, **grid_options) # Layout phase frame self.phase_frame.grid_columnconfigure((0, 1, 2), weight=1) @@ -360,17 +415,188 @@ def setup_config_frame(self): self.limit_sensor_frame.grid(column=1, row=0, **grid_options) self.sensor_test_switch.grid(column=0, row=1, columnspan=2, **grid_options) + # Layout general tab frame + self.general_tab_frame.grid_columnconfigure((0, 1), weight=1) + self.general_tab_frame.grid_rowconfigure((0, 1, 2), weight=1) + self.main_options_frame.grid(column=0, row=0) + self.phase_frame.grid(column=0, row=1) + self.sensor_frame.grid(column=0, row=2) + + # Create stepper frames for grouping + self.stepper_frame = ctk.CTkFrame(self.stepper_tab_frame, width=760, border_width=0) + self.stepper_speed_frame = ctk.CTkFrame(self.stepper_tab_frame, width=760, border_width=0) + self.stepper_switch_frame = ctk.CTkFrame(self.stepper_tab_frame, width=760, border_width=0) + + # Create stepper selection widgets + self.stepper_label = ctk.CTkLabel(self.stepper_frame, text="Select the stepper driver:", + font=self.instruction_font) + CreateToolTip(self.stepper_label, stepper_tip, + "https://dcc-ex.com/ex-turntable/purchasing.html#supported-stepper-drivers-and-motors") + self.stepper_combo = ctk.CTkComboBox(self.stepper_frame, values=["Select stepper driver"], + width=200, command=self.check_stepper) + self.gearing_label = ctk.CTkLabel(self.stepper_frame, text="Set the gearing factor", + font=self.instruction_font) + CreateToolTip(self.gearing_label, gear_factor_tip, + "https://dcc-ex.com/ex-turntable/configure.html#stepper-gearing-factor") + self.gearing = ctk.StringVar(self, value="1") + self.gearing_entry = ctk.CTkEntry(self.stepper_frame, textvariable=self.gearing, font=self.instruction_font, + width=40) + + # Create stepper tuning widgets + self.speed_label = ctk.CTkLabel(self.stepper_speed_frame, text="Set the stepper top speed", + font=self.instruction_font) + CreateToolTip(self.speed_label, speed_tip) + self.speed = ctk.StringVar(self, value="200") + self.speed_entry = ctk.CTkEntry(self.stepper_speed_frame, textvariable=self.speed, font=self.instruction_font, + width=60) + self.accel_label = ctk.CTkLabel(self.stepper_speed_frame, text="Set acceleration rate", + font=self.instruction_font) + CreateToolTip(self.accel_label, accel_tip) + self.accel = ctk.StringVar(self, value="25") + self.accel_entry = ctk.CTkEntry(self.stepper_speed_frame, textvariable=self.accel, font=self.instruction_font, + width=40) + + # Create other stepper switch widgets + stepper_switch_width = 220 + self.disable_idle_switch = ctk.CTkSwitch(self.stepper_switch_frame, text="Disable stepper when idle", + onvalue="on", offvalue="off", font=self.instruction_font, + width=stepper_switch_width) + self.disable_idle_switch.select() + CreateToolTip(self.disable_idle_switch, idle_tip) + self.invert_dir_switch = ctk.CTkSwitch(self.stepper_switch_frame, text="Invert direction pin", + onvalue="on", offvalue="off", font=self.instruction_font, + width=stepper_switch_width) + CreateToolTip(self.invert_dir_switch, invert_direction_tip) + self.invert_step_switch = ctk.CTkSwitch(self.stepper_switch_frame, text="Invert step pin", + onvalue="on", offvalue="off", font=self.instruction_font, + width=stepper_switch_width) + CreateToolTip(self.invert_step_switch, invert_step_tip) + self.invert_enable_switch = ctk.CTkSwitch(self.stepper_switch_frame, text="Invert enable pin", + onvalue="on", offvalue="off", font=self.instruction_font, + width=stepper_switch_width) + CreateToolTip(self.invert_enable_switch, invert_enable_tip) + self.forward_only_switch = ctk.CTkSwitch(self.stepper_switch_frame, text="Force forward rotation only", + onvalue="on", offvalue="off", font=self.instruction_font, + width=stepper_switch_width, command=self.set_forward_only) + CreateToolTip(self.forward_only_switch, forward_only_tip) + self.reverse_only_switch = ctk.CTkSwitch(self.stepper_switch_frame, text="Force reverse rotation only", + onvalue="on", offvalue="off", font=self.instruction_font, + width=stepper_switch_width, command=self.set_reverse_only) + CreateToolTip(self.reverse_only_switch, reverse_only_tip) + + # Layout stepper frame + self.stepper_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.stepper_frame.grid_rowconfigure((0, 1), weight=1) + self.stepper_label.grid(column=0, row=0, sticky="e", **grid_options) + self.stepper_combo.grid(column=1, row=0, columnspan=2, sticky="w", **grid_options) + self.gearing_label.grid(column=2, row=0, sticky="e", **grid_options) + self.gearing_entry.grid(column=3, row=0, sticky="w", **grid_options) + + # Layout stepper speed frame + self.stepper_speed_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.stepper_speed_frame.grid_rowconfigure(0, weight=1) + self.speed_label.grid(column=0, row=0, sticky="e", **grid_options) + self.speed_entry.grid(column=1, row=0, sticky="w", **grid_options) + self.accel_label.grid(column=2, row=0, sticky="e", **grid_options) + self.accel_entry.grid(column=3, row=0, sticky="w", **grid_options) + + # Layout stepper switches + self.stepper_switch_frame.grid_columnconfigure((0, 1, 2), weight=1) + self.stepper_switch_frame.grid_rowconfigure((0, 1), weight=1) + self.disable_idle_switch.grid(column=0, row=0, **grid_options) + self.forward_only_switch.grid(column=1, row=0, **grid_options) + self.reverse_only_switch.grid(column=2, row=0, **grid_options) + self.invert_dir_switch.grid(column=0, row=1, **grid_options) + self.invert_step_switch.grid(column=1, row=1, **grid_options) + self.invert_enable_switch.grid(column=2, row=1, **grid_options) + + # Create advanced widgets + adv_switch_width = 220 + self.led_fast = ctk.StringVar(self, value=100) + self.led_fast_switch = ctk.CTkSwitch(self.advanced_tab_frame, text="Override fast LED blink rate", + onvalue="on", offvalue="off", font=self.instruction_font, + width=adv_switch_width) + CreateToolTip(self.led_fast_switch, text=advanced_tip, + url="https://dcc-ex.com/ex-turntable/configure.html#advanced-configuration-options") + self.led_fast_entry = ctk.CTkEntry(self.advanced_tab_frame, textvariable=self.led_fast, + font=self.instruction_font, width=60) + self.led_slow = ctk.StringVar(self, value=500) + self.led_slow_switch = ctk.CTkSwitch(self.advanced_tab_frame, text="Override slow LED blink rate", + onvalue="on", offvalue="off", font=self.instruction_font, + width=adv_switch_width) + CreateToolTip(self.led_slow_switch, text=advanced_tip, + url="https://dcc-ex.com/ex-turntable/configure.html#advanced-configuration-options") + self.led_slow_entry = ctk.CTkEntry(self.advanced_tab_frame, textvariable=self.led_slow, + font=self.instruction_font, width=60) + self.sanity_steps = ctk.StringVar(self, value=10000) + self.sanity_steps_switch = ctk.CTkSwitch(self.advanced_tab_frame, text="Override sanity step count", + onvalue="on", offvalue="off", font=self.instruction_font, + width=adv_switch_width) + CreateToolTip(self.sanity_steps_switch, text=advanced_tip, + url="https://dcc-ex.com/ex-turntable/configure.html#advanced-configuration-options") + self.sanity_steps_entry = ctk.CTkEntry(self.advanced_tab_frame, textvariable=self.sanity_steps, + font=self.instruction_font, width=60) + self.home_sensitivity = ctk.StringVar(self, value=300) + self.home_sensitivity_switch = ctk.CTkSwitch(self.advanced_tab_frame, text="Override homing sensitivity", + onvalue="on", offvalue="off", font=self.instruction_font, + width=adv_switch_width) + CreateToolTip(self.home_sensitivity_switch, text=advanced_tip, + url="https://dcc-ex.com/ex-turntable/configure.html#advanced-configuration-options") + self.home_sensitivity_entry = ctk.CTkEntry(self.advanced_tab_frame, textvariable=self.home_sensitivity, + font=self.instruction_font, width=60) + self.full_step_count = ctk.StringVar(self, value=4096) + self.full_step_count_switch = ctk.CTkSwitch(self.advanced_tab_frame, text="Override full step count", + onvalue="on", offvalue="off", font=self.instruction_font, + width=adv_switch_width) + CreateToolTip(self.full_step_count_switch, text=advanced_tip, + url="https://dcc-ex.com/ex-turntable/configure.html#advanced-configuration-options") + self.full_step_count_entry = ctk.CTkEntry(self.advanced_tab_frame, textvariable=self.full_step_count, + font=self.instruction_font, width=60) + self.debounce_delay = ctk.StringVar(self, value=10) + self.debounce_delay_switch = ctk.CTkSwitch(self.advanced_tab_frame, text="Override debounce delay", + onvalue="on", offvalue="off", font=self.instruction_font, + width=adv_switch_width) + CreateToolTip(self.debounce_delay_switch, text=advanced_tip, + url="https://dcc-ex.com/ex-turntable/configure.html#advanced-configuration-options") + self.debounce_delay_entry = ctk.CTkEntry(self.advanced_tab_frame, textvariable=self.debounce_delay, + font=self.instruction_font, width=60) + self.debug_switch = ctk.CTkSwitch(self.advanced_tab_frame, text="Enable debug output", + onvalue="on", offvalue="off", font=self.instruction_font, + width=adv_switch_width) + CreateToolTip(self.debug_switch, text=advanced_tip, + url="https://dcc-ex.com/ex-turntable/configure.html#advanced-configuration-options") + + # Layout advanced tab frame + self.advanced_tab_frame.grid_columnconfigure((0, 1, 2, 3), weight=1) + self.advanced_tab_frame.grid_rowconfigure((0, 1, 2, 3), weight=1) + self.led_fast_switch.grid(column=0, row=0, sticky="e", **grid_options) + self.led_fast_entry.grid(column=1, row=0, sticky="w", **grid_options) + self.led_slow_switch.grid(column=2, row=0, sticky="e", **grid_options) + self.led_slow_entry.grid(column=3, row=0, sticky="w", **grid_options) + self.sanity_steps_switch.grid(column=0, row=1, sticky="e", **grid_options) + self.sanity_steps_entry.grid(column=1, row=1, sticky="w", **grid_options) + self.home_sensitivity_switch.grid(column=2, row=1, sticky="e", **grid_options) + self.home_sensitivity_entry.grid(column=3, row=1, sticky="w", **grid_options) + self.full_step_count_switch.grid(column=0, row=2, sticky="e", **grid_options) + self.full_step_count_entry.grid(column=1, row=2, sticky="w", **grid_options) + self.debounce_delay_switch.grid(column=2, row=2, sticky="e", **grid_options) + self.debounce_delay_entry.grid(column=3, row=2, sticky="w", **grid_options) + self.debug_switch.grid(column=0, row=3, sticky="e", **grid_options) + # Layout main config frame frame_grid_options = {"sticky": "ew", "padx": 30, "pady": 5} + self.config_frame.grid_columnconfigure(0, weight=1) + self.config_frame.grid_rowconfigure(1, weight=1) self.instruction_label.grid(column=0, row=0, **grid_options) - self.main_options_frame.grid(column=0, row=1, **frame_grid_options) - self.stepper_frame.grid(column=0, row=2, **frame_grid_options) - self.phase_frame.grid(column=0, row=3, **frame_grid_options) - self.sensor_frame.grid(column=0, row=4, **frame_grid_options) + self.stepper_tab_frame.grid_columnconfigure(0, weight=1) + self.stepper_tab_frame.grid_rowconfigure((0, 1, 2), weight=1) + self.stepper_frame.grid(column=0, row=0, **frame_grid_options) + self.stepper_speed_frame.grid(column=0, row=1, **frame_grid_options) + self.stepper_switch_frame.grid(column=0, row=2, **frame_grid_options) self.advanced_config_enabled.grid(column=0, row=5, sticky="e", **grid_options) + self.config_tabview.grid(column=0, row=1, sticky="nsew", **grid_options) # Set toggles - self.get_steppers() self.check_stepper(self.stepper_combo.get()) self.set_home() self.set_limit() @@ -419,6 +645,8 @@ def validate_i2c_address(self, event=None): def set_mode(self): """ Highlight the chosen option for the mode toggle switch + + In traverser mode, ensure forward/reverse only options are deselected and disabled """ if self.mode_switch.get() == "TURNTABLE": self.turntable_label.configure(font=self.bold_instruction_font) @@ -427,6 +655,8 @@ def set_mode(self): self.limit_switch.configure(fg_color="#939BA2", progress_color="#939BA2") self.limit_high_label.configure(font=self.small_italic_instruction_font, text_color="white") self.limit_low_label.configure(font=self.small_italic_instruction_font, text_color="white") + self.forward_only_switch.configure(state="normal") + self.reverse_only_switch.configure(state="normal") else: self.turntable_label.configure(font=self.small_italic_instruction_font) self.traverser_label.configure(font=self.bold_instruction_font) @@ -434,6 +664,10 @@ def set_mode(self): self.limit_switch.configure(fg_color="#00A3B9", progress_color="#00A3B9") self.limit_high_label.configure(text_color="#00353D") self.limit_low_label.configure(text_color="#00353D") + self.forward_only_switch.deselect() + self.forward_only_switch.configure(state="disabled") + self.reverse_only_switch.deselect() + self.reverse_only_switch.configure(state="disabled") self.set_limit() def set_home(self): @@ -495,7 +729,7 @@ def get_steppers(self): def check_stepper(self, value): """ - Function ensure a motor driver has been selected + Function to ensure a motor driver has been selected """ if value == "Select stepper driver": self.next_back.disable_next() @@ -507,10 +741,24 @@ def set_advanced_config(self): Sets next screen to be config editing rather than compile/upload """ if self.advanced_config_enabled.get() == "on": - self.next_back.set_next_text("Advanced config") + self.next_back.set_next_text("Edit config") else: self.next_back.set_next_text("Compile and load") + def set_forward_only(self): + """ + Ensures reverse only switch is deselected if forward only is set + """ + if self.reverse_only_switch.get() == "on": + self.reverse_only_switch.deselect() + + def set_reverse_only(self): + """ + Ensures forward only switch is deselected if reverse only is set + """ + if self.forward_only_switch.get() == "on": + self.forward_only_switch.deselect() + def generate_config(self): """ Validates all configuration parameters and if valid generates myConfig.h @@ -527,6 +775,8 @@ def generate_config(self): config_list.append(f"#define TURNTABLE_EX_MODE {self.mode_switch.get()}\n") if self.sensor_test_switch.get() == "on": config_list.append("#define SENSOR_TESTING\n") + else: + config_list.append("// #define SENSOR_TESTING\n") config_list.append(f"#define HOME_SENSOR_ACTIVE_STATE {self.home_switch.get()}\n") config_list.append(f"#define LIMIT_SENSOR_ACTIVE_STATE {self.limit_switch.get()}\n") config_list.append(f"#define RELAY_ACTIVE_STATE {self.relay_switch.get()}\n") @@ -566,6 +816,105 @@ def generate_config(self): param_errors.append("Acceleration must be between 1 and 1000") else: config_list.append(f"#define STEPPER_ACCELERATION {self.accel.get()}\n") + if self.gearing_entry.cget("state") == "normal": + try: + int(self.gearing.get()) + except Exception: + param_errors.append("You must provide a numeric gearing factor") + else: + if (int(self.gearing.get()) < 1 or int(self.gearing.get()) > 10): + param_errors.append("Gearing factor must be between 1 and 10") + else: + config_list.append(f"#define STEPPER_GEARING_FACTOR {self.gearing.get()}\n") + if self.invert_dir_switch.cget("state") == "normal": + if self.invert_dir_switch.get() == "on": + config_list.append("#define INVERT_DIRECTION\n") + else: + config_list.append("// #define INVERT_DIRECTION\n") + if self.invert_step_switch.cget("state") == "normal": + if self.invert_step_switch.get() == "on": + config_list.append("#define INVERT_STEP\n") + else: + config_list.append("// #define INVERT_STEP\n") + if self.invert_enable_switch.cget("state") == "normal": + if self.invert_enable_switch.get() == "on": + config_list.append("#define INVERT_ENABLE\n") + else: + config_list.append("// #define INVERT_ENABLE\n") + if self.forward_only_switch.cget("state") == "normal": + if self.forward_only_switch.get() == "on": + if self.mode_switch.get() == "TRAVERSER": + param_errors.append("Traverser mode is incompatible with forward only rotation") + else: + config_list.append("#define ROTATE_FORWARD_ONLY\n") + else: + config_list.append("// #define ROTATE_FORWARD_ONLY\n") + if self.reverse_only_switch.cget("state") == "normal": + if self.reverse_only_switch.get() == "on": + if self.mode_switch.get() == "TRAVERSER": + param_errors.append("Traverser mode is incompatible with reverse only rotation") + else: + config_list.append("#define ROTATE_REVERSE_ONLY\n") + else: + config_list.append("// #define ROTATE_REVERSE_ONLY\n") + if self.led_fast_switch.get() == "on": + try: + int(self.led_fast.get()) + except Exception: + param_errors.append("Fast LED delay must be numeric") + else: + config_list.append(f"#define LED_FAST {self.led_fast.get()}\n") + else: + config_list.append("#define LED_FAST 100\n") + if self.led_slow_switch.get() == "on": + try: + int(self.led_slow.get()) + except Exception: + param_errors.append("Slow LED delay must be numeric") + else: + config_list.append(f"#define LED_SLOW {self.led_slow.get()}\n") + else: + config_list.append("#define LED_SLOW 500\n") + if self.debug_switch.get() == "on": + config_list.append("#define DEBUG\n") + else: + config_list.append("// #define DEBUG\n") + if self.sanity_steps_switch.get() == "on": + try: + int(self.sanity_steps.get()) + except Exception: + param_errors.append("Sanity step count must be numeric") + else: + config_list.append(f"#define SANITY_STEPS {self.sanity_steps.get()}\n") + else: + config_list.append("// #define SANITY_STEPS 10000\n") + if self.home_sensitivity_switch.get() == "on": + try: + int(self.home_sensitivity.get()) + except Exception: + param_errors.append("Home sensitivity step count must be numeric") + else: + config_list.append(f"#define HOME_SENSITIVITY {self.home_sensitivity.get()}\n") + else: + config_list.append("// #define HOME_SENSITIVITY 300\n") + if self.full_step_count_switch.get() == "on": + try: + int(self.full_step_count.get()) + except Exception: + param_errors.append("Full step count must be numeric") + else: + config_list.append(f"#define FULL_STEP_COUNT {self.full_step_count.get()}\n") + else: + config_list.append("// #define FULL_STEP_COUNT 4096\n") + if self.debounce_delay_switch.get() == "on": + try: + int(self.debounce_delay.get()) + except Exception: + param_errors.append("Debounce delay must be numeric") + else: + config_list.append(f"#define DEBOUNCE_DELAY {self.debounce_delay.get()}\n") + else: + config_list.append("// #define DEBOUNCE_DELAY 10\n") if len(param_errors) > 0: message = ", ".join(param_errors) self.process_error(message) @@ -575,7 +924,6 @@ def generate_config(self): f"v{self.app_version} for {self.product_name} " + f"{self.product_version_name}\n\n")] file_contents += config_list - file_contents += self.advanced_config_options config_file_path = fm.get_filepath(self.ex_turntable_dir, "config.h") write_config = fm.write_config_file(config_file_path, file_contents) if write_config != config_file_path: diff --git a/ex_installer/select_version_config.py b/ex_installer/select_version_config.py index 2968693..8e33c65 100644 --- a/ex_installer/select_version_config.py +++ b/ex_installer/select_version_config.py @@ -373,7 +373,9 @@ def copy_config_files(self): """ copy_list = fm.get_config_files(self.config_path.get(), pd[self.product]["minimum_config_files"]) if copy_list: - extra_list = fm.get_config_files(self.config_path.get(), pd[self.product]["other_config_files"]) + extra_list = None + if "other_config_files" in pd[self.product]: + extra_list = fm.get_config_files(self.config_path.get(), pd[self.product]["other_config_files"]) if extra_list: copy_list += extra_list file_copy = fm.copy_config_files(self.config_path.get(), self.product_dir, copy_list) diff --git a/ex_installer/serial_monitor.py b/ex_installer/serial_monitor.py index 29d495f..1556f19 100644 --- a/ex_installer/serial_monitor.py +++ b/ex_installer/serial_monitor.py @@ -17,10 +17,12 @@ import sys import serial import re +from datetime import datetime # Import local modules from . import images from .common_fonts import CommonFonts +from .file_manager import FileManager as fm # Define valid monitor highlights monitor_highlights = { @@ -129,8 +131,10 @@ def __init__(self, parent, *args, **kwargs): self.window_frame.grid(column=0, row=0, sticky="nsew") # Define fonts for use - button_font = self.common_fonts.button_font - instruction_font = self.common_fonts.instruction_font + self.button_font = self.common_fonts.button_font + self.instruction_font = self.common_fonts.instruction_font + self.bold_instruction_font = self.common_fonts.bold_instruction_font + self.action_button_font = self.common_fonts.action_button_font self.command_frame = ctk.CTkFrame(self.window_frame, width=790, height=40) self.monitor_frame = ctk.CTkFrame(self.window_frame, width=790, height=420) @@ -148,19 +152,22 @@ def __init__(self, parent, *args, **kwargs): # Create command frame widgets and layout frame self.command_history = [] grid_options = {"padx": 5, "pady": 5} - self.command_label = ctk.CTkLabel(self.command_frame, text="Enter command:", font=instruction_font) + self.command_label = ctk.CTkLabel(self.command_frame, text="Enter command:", font=self.instruction_font) self.command = ctk.StringVar(self) self.command_entry = ctk.CTkComboBox(self.command_frame, variable=self.command, values=self.command_history, command=self.send_command) self.command_entry.bind("", self.send_command) - self.command_button = ctk.CTkButton(self.command_frame, text="Send", font=button_font, width=80, + self.command_button = ctk.CTkButton(self.command_frame, text="Send", font=self.button_font, width=80, command=self.send_command) - self.close_button = ctk.CTkButton(self.command_frame, text="Close", font=button_font, width=80, + self.save_log_button = ctk.CTkButton(self.command_frame, text="Save Log", font=self.button_font, width=80, + command=self.show_save_log_popup) + self.close_button = ctk.CTkButton(self.command_frame, text="Close", font=self.button_font, width=80, command=self.close_monitor) self.command_label.grid(column=0, row=0, sticky="w", **grid_options) self.command_entry.grid(column=1, row=0, sticky="ew", **grid_options) self.command_button.grid(column=2, row=0, sticky="e", pady=5) - self.close_button.grid(column=3, row=0, sticky="e", **grid_options) + self.save_log_button.grid(column=3, row=0, sticky="e", padx=(5, 0), pady=5) + self.close_button.grid(column=4, row=0, sticky="e", **grid_options) # Create monitor frame widgets and layout frame self.output_textbox = ctk.CTkTextbox(self.monitor_frame, border_width=3, border_spacing=5, @@ -190,7 +197,7 @@ def __init__(self, parent, *args, **kwargs): foreground="white") # Create device frame widgets and layout - self.device_label = ctk.CTkLabel(self.device_frame, text=None, font=instruction_font) + self.device_label = ctk.CTkLabel(self.device_frame, text=None, font=self.instruction_font) self.device_label.grid(column=0, row=0, sticky="ew", padx=5, pady=5) # Start serial monitor process @@ -293,6 +300,103 @@ def send_command(self, event=None): self.output_textbox.configure(state="disabled") self.output_textbox.see("end") + def show_save_log_popup(self): + """ + Function to show the message box to select the folder to save device log + """ + if hasattr(self, "log_popup") and self.log_popup is not None and self.log_popup.winfo_exists(): + self.log_popup.focus() + else: + self.log_popup = ctk.CTkToplevel(self) + self.log_popup.focus() + self.log_popup.lift(self) + + # Ensure pop up is within the confines of the app window to start + main_window = self.winfo_toplevel() + log_offset_x = main_window.winfo_x() + 75 + log_offset_y = main_window.winfo_y() + 200 + self.log_popup.geometry(f"+{log_offset_x}+{log_offset_y}") + self.log_popup.update() + + # Set icon and title + if sys.platform.startswith("win"): + self.log_popup.after(250, lambda icon=images.DCC_EX_ICON_ICO: self.log_popup.iconbitmap(icon)) + self.log_popup.title("Save device log") + self.log_popup.withdraw() + self.log_popup.after(250, self.log_popup.deiconify) + self.log_popup.grid_columnconfigure(0, weight=1) + self.log_popup.grid_rowconfigure(0, weight=1) + self.window_frame = ctk.CTkFrame(self.log_popup, fg_color="grey95") + self.window_frame.grid_columnconfigure((0, 1, 2), weight=1) + self.window_frame.grid_rowconfigure((0, 1), weight=1) + self.window_frame.grid(column=0, row=0, sticky="nsew") + self.folder_label = ctk.CTkLabel(self.window_frame, text="Select the folder to save the log:", + font=self.instruction_font) + self.status_frame = ctk.CTkFrame(self.window_frame, border_width=2) + self.status_label = ctk.CTkLabel(self.status_frame, text="Status:", + font=self.instruction_font) + self.status_text = ctk.CTkLabel(self.status_frame, text="Enter or select destination", + font=self.bold_instruction_font) + self.log_path = ctk.StringVar(value=None) + self.log_path_entry = ctk.CTkEntry(self.window_frame, textvariable=self.log_path, + width=300) + self.browse_button = ctk.CTkButton(self.window_frame, text="Browse", + width=80, command=self.browse_log_dir) + self.save_button = ctk.CTkButton(self.window_frame, width=200, height=50, + text="Save and open log", font=self.action_button_font, + command=self.save_log_file) + self.folder_label.grid(column=0, row=0, padx=(10, 1), pady=(10, 5)) + self.log_path_entry.grid(column=1, row=0, padx=1, pady=(10, 5)) + self.browse_button.grid(column=2, row=0, padx=(1, 10), pady=(10, 5)) + self.save_button.grid(column=0, row=1, columnspan=3, padx=10, pady=5) + self.status_frame.grid_columnconfigure((0, 1), weight=1) + self.status_frame.grid_rowconfigure(0, weight=1) + self.status_frame.grid(column=0, row=2, columnspan=3, sticky="nsew", padx=10, pady=(5, 10)) + self.status_label.grid(column=0, row=0, padx=5, pady=5) + self.status_text.grid(column=1, row=0, padx=5, pady=5) + + def browse_log_dir(self): + """ + Opens a directory browser dialogue to select the folder to save the device log to + """ + directory = ctk.filedialog.askdirectory() + if directory: + self.log_path.set(directory) + self.log.debug("Save device log to %s", directory) + self.log_popup.focus() + + def save_log_file(self): + """ + Function to save the device log file to the chosen location, and open it + """ + if fm.is_valid_dir(self.log_path.get()): + log_name = datetime.now().strftime("device-logs-%Y%m%d-%H%M%S.log") + log_file = os.path.join(self.log_path.get(), log_name) + file_contents = self.output_textbox.get("1.0", ctk.END) + try: + with open(log_file, "w", encoding="utf-8") as f: + f.writelines(file_contents) + f.close() + except Exception as error: + message = "Unable to save device log" + self.status_text.configure(text=message, text_color="red") + self.log.error("Failed to save device log: %s", error) + else: + self.log_popup.destroy() + self.focus() + if platform.system() == "Darwin": + subprocess.call(("open", log_file)) + elif platform.system() == "Windows": + os.startfile(log_file) + else: + subprocess.call(("xdg-open", log_file)) + else: + if self.log_path.get() == "": + message = "You must specify a valid folder to save to" + else: + message = f"{self.log_path.get()} is not a valid directory" + self.status_text.configure(text=message, text_color="red") + def exception_handler(self, exc_type, exc_value, exc_traceback): """ Handler for uncaught exceptions diff --git a/ex_installer/version.py b/ex_installer/version.py index d474a6e..9313503 100644 --- a/ex_installer/version.py +++ b/ex_installer/version.py @@ -7,11 +7,17 @@ read by the application build process to embed in the application details """ -ex_installer_version = "0.0.17" +ex_installer_version = "0.0.18" """ Version history: +0.0.18 - Update EX-Turntable configuration options to suit changes in 0.7.0 + - Dependabot update for cryptography to 42.0.4 + - Add link to DCC-EX News articles about EX-Installer to the Info menu + - Ensure the config backup popup is always launched within the app window geometry + - Add a save log button to device monitor to save the device logs to a text file + - Fix bug where copying existing config files for EX-Turntable and EX-IOExpander causes an exception 0.0.17 - Move fonts to a separate common class - Change default font to Arial for Windows/Mac and FreeSans for Linux - Update various modules versions to resolve Dependapot identified vulnerabilities diff --git a/requirements.txt b/requirements.txt index 31069f1..a450871 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ attrs==23.1.0 certifi==2023.7.22 cffi==1.15.1 charset-normalizer==2.1.1 -cryptography==41.0.6 +cryptography==42.0.4 CTkMessagebox==2.0 customtkinter==5.1.2 darkdetect==0.8.0