├── requirements.txt ├── img ├── connector.jpg └── connector.png ├── extras ├── __pycache__ │ └── ace.cpython-311.pyc └── ace.py ├── KS ├── KlipperScreen.conf ├── readme.md └── acepro.py ├── saved_variables.cfg ├── ace.cfg ├── PROTOCOL.md ├── README.md ├── LICENSE └── LICENSE.md /requirements.txt: -------------------------------------------------------------------------------- 1 | pyserial==3.5 2 | pyjson==1.4.1 -------------------------------------------------------------------------------- /img/connector.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szkrisz/ACEPROSV08/HEAD/img/connector.jpg -------------------------------------------------------------------------------- /img/connector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szkrisz/ACEPROSV08/HEAD/img/connector.png -------------------------------------------------------------------------------- /extras/__pycache__/ace.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szkrisz/ACEPROSV08/HEAD/extras/__pycache__/ace.cpython-311.pyc -------------------------------------------------------------------------------- /KS/KlipperScreen.conf: -------------------------------------------------------------------------------- 1 | [menu __main] 2 | name: {{ gettext('Main Menu') }} 3 | 4 | [menu __main acepro] 5 | name: ACE Pro 6 | icon: settings 7 | panel: acepro 8 | 9 | #~# --- Do not edit below this line. This section is auto generated --- #~# 10 | #~# 11 | #~# [main] 12 | #~# print_sort_dir = date_desc 13 | #~# 14 | #~# [graph Printer] 15 | #~# heater_bed = False 16 | #~# 17 | -------------------------------------------------------------------------------- /saved_variables.cfg: -------------------------------------------------------------------------------- 1 | [Variables] 2 | ace_current_index = -1 3 | ace_endless_spool_enabled = True 4 | ace_filament_pos = 'spliter' 5 | ace_inventory = [{'status': 'empty', 'color': [0, 0, 0], 'material': '', 'temp': 0}, {'status': 'ready', 'color': [255, 255, 255], 'material': 'PETG', 'temp': 250}, {'status': 'ready', 'color': [255, 255, 255], 'material': 'PETG', 'temp': 250}, {'status': 'ready', 'color': [255, 255, 255], 'material': 'PETG', 'temp': 250}] 6 | ace_status = 0 7 | filepath = '' 8 | last_file = '' 9 | nvm_offset = 0 10 | offsetadjust = 0.0 11 | power_resume_z = 0 12 | show_freq = '' 13 | was_interrupted = False 14 | x_freq = 0 15 | y_freq = 0 16 | 17 | -------------------------------------------------------------------------------- /ace.cfg: -------------------------------------------------------------------------------- 1 | # Please check that [save_variables] is above [ace] if you're using different config 2 | [save_variables] 3 | filename: ~/printer_data/config/saved_variables.cfg 4 | 5 | [respond] 6 | 7 | [ace] 8 | #serial: /dev/serial/by-id/usb-ANYCUBIC_ACE_1-if00 9 | serial: /dev/ttyACM0 10 | baud: 115200 11 | # Default feeding speed, 10-25 in stock 12 | feed_speed: 80 13 | # Default retraction speed, 10-25 in stock 14 | retract_speed: 80 15 | # Length of the retract to make for toolchange 16 | toolchange_retract_length: 150 17 | #toolhead_sensor_to_nozzle: 62 18 | toolchange_load_length: 630 19 | # Length of bowden tube between the ACEPRO and the splitter (default: 1000mm) 20 | bowden_tube_length: 1000 21 | # Park to toolhead hit count, default is 5, can be lowered if your setup works stably on lower values 22 | #park_hit_count: 16 23 | max_dryer_temperature: 55 24 | # Disables feed assist after toolchange. Defaults to true 25 | #disable_assist_after_toolchange: true 26 | toolhead_sensor_to_nozzle: 50 27 | extruder_sensor_pin: PE9 28 | toolhead_sensor_pin: !extra_mcu:PA3 29 | 30 | [gcode_macro CUT_TIP] 31 | gcode: 32 | RESPOND TYPE=echo MSG="CUTING..." 33 | {% if "xyz" not in printer.toolhead.homed_axes %} 34 | G28 35 | {% endif %} 36 | SAVE_GCODE_STATE NAME=my_move_up_state 37 | M117 Cutting 38 | G1 X25 F6000 39 | G1 Y0 F6000 40 | M400 41 | G1 X10 F600 42 | G1 X25 F6000 43 | G1 X10 F600 44 | G1 X25 F6000 45 | M400 46 | FORCE_MOVE STEPPER=extruder DISTANCE=-50 VELOCITY=10 47 | M117 CUT DONE... 48 | M400 49 | RESPOND TYPE=echo MSG="CUT DONE" 50 | RESTORE_GCODE_STATE NAME=my_move_up_state MOVE=1 MOVE_SPEED=200 51 | 52 | [gcode_macro _ACE_PRE_TOOLCHANGE] 53 | variable_purge_temp_min: 240 54 | gcode: 55 | {action_respond_info("Doing Pre toolchange")} 56 | SAVE_GCODE_STATE NAME=TOOLCHANGE 57 | {% if "xyz" not in printer.toolhead.homed_axes %} 58 | G28 59 | {% endif %} 60 | G91 61 | G1 Z2 F100 ; Up Z slightly 62 | M400 63 | G90 ; Return to absolute positioning 64 | G1 X125 Y360 F6000 65 | M400 66 | {% if printer.extruder.temperature < purge_temp_min %} 67 | {% if printer.extruder.target < purge_temp_min %} 68 | M109 S{purge_temp_min} 69 | {% else %} 70 | TEMPERATURE_WAIT SENSOR=extruder MINIMUM={purge_temp_min} 71 | {% endif %} 72 | {% endif %} 73 | 74 | #G92 E0 75 | {action_respond_info("Doing Toolchange")} 76 | 77 | 78 | 79 | [gcode_macro _ACE_POST_TOOLCHANGE] 80 | gcode: 81 | {action_respond_info("Doing Post toolchange")} 82 | G91 83 | G1 Z-2 F100 ; Lower back Z slightly 84 | M400 85 | G90 ; Return to absolute positioning 86 | #CLEAN NOZZLE 87 | #G1 X300 Y356 F1200 88 | #G1 X320 Y356 F1200 89 | #M400 90 | RESTORE_GCODE_STATE NAME=TOOLCHANGE MOVE=1 MOVE_SPEED=200 91 | 92 | {action_respond_info("Finish Toolchange")} 93 | 94 | 95 | [gcode_macro _ACE_ON_EMPTY_ERROR] 96 | gcode: 97 | {action_respond_info("Spool is empty")} 98 | {% if printer.idle_timeout.state == "Printing" %} 99 | PAUSE 100 | {% endif %} 101 | 102 | 103 | [gcode_macro TR] 104 | gcode: 105 | ACE_CHANGE_TOOL TOOL=-1 106 | 107 | [gcode_macro T0] 108 | gcode: 109 | ACE_CHANGE_TOOL TOOL=0 110 | 111 | [gcode_macro T1] 112 | gcode: 113 | ACE_CHANGE_TOOL TOOL=1 114 | 115 | [gcode_macro T2] 116 | gcode: 117 | ACE_CHANGE_TOOL TOOL=2 118 | 119 | [gcode_macro T3] 120 | gcode: 121 | ACE_CHANGE_TOOL TOOL=3 122 | 123 | -------------------------------------------------------------------------------- /KS/readme.md: -------------------------------------------------------------------------------- 1 | # ACE Pro Panel Installation Guide 2 | 3 | A custom KlipperScreen panel for controlling the Anycubic Color Engine Pro (ACE Pro) multimaterial unit. This panel provides a touchscreen interface for managing filament slots, endless spool functionality, and dryer controls. 4 | 5 | ## Features 6 | 7 | - **Slot Management**: Configure and control 4 filament slots with material type, color, and temperature settings 8 | - **Visual Status**: Real-time display of loaded slot with color-coded indicators 9 | - **Endless Spool**: Global enable/disable control for automatic filament switching 10 | - **Two-Column Configuration**: Compact interface optimized for 480px height touchscreens 11 | - **Material Selection**: Support for PLA, ABS, PETG, TPU, ASA, PVA, HIPS, and PC 12 | - **Color Picker**: RGB color selection with presets and sliders 13 | - **Temperature Control**: Keypad input for precise temperature settings 14 | - **Dryer Control**: Start/stop filament drying with temperature selection 15 | - **Load/Unload**: Direct slot loading and unloading with confirmation dialogs 16 | 17 | ## Prerequisites 18 | 19 | 1. **Klipper with ACE Pro Driver**: The ACEPROSV08 driver must be installed and configured in your Klipper setup 20 | 2. **KlipperScreen**: A working KlipperScreen installation 21 | 3. **saved_variables**: The `saved_variables.cfg` file must be configured in Klipper (required by ACE driver) 22 | 23 | ## Installation 24 | 25 | ### Step 1: Copy the Panel File 26 | 27 | Copy the `acepro.py` file to your KlipperScreen panels directory: 28 | 29 | ```bash 30 | # Navigate to KlipperScreen directory 31 | cd ~/KlipperScreen 32 | 33 | # Copy the panel file 34 | cp /path/to/acepro.py panels/ 35 | ``` 36 | 37 | ### Step 2: Configure KlipperScreen Menu 38 | 39 | Add the ACE Pro panel to your KlipperScreen configuration. Edit your `KlipperScreen.conf` file: 40 | 41 | ```bash 42 | nano ~/printer_data/config/KlipperScreen.conf 43 | ``` 44 | 45 | Add the following section: 46 | 47 | ```ini 48 | [menu __main acepro] 49 | name: ACE Pro 50 | icon: filament 51 | panel: acepro 52 | ``` 53 | 54 | ### Step 3: Ensure ACE Driver Commands 55 | 56 | Make sure your Klipper configuration includes the ACE Pro driver with these required commands: 57 | - `ACE_QUERY_SLOTS`: Returns slot configuration data 58 | - `ACE_GET_CURRENT_INDEX`: Returns currently loaded slot (0-3) or -1 if none 59 | - `ACE_SET_SLOT`: Configure slot parameters 60 | - `ACE_CHANGE_TOOL`: Load/unload slots 61 | - `ACE_ENABLE_ENDLESS_SPOOL`: Enable endless spool 62 | - `ACE_DISABLE_ENDLESS_SPOOL`: Disable endless spool 63 | - `ACE_ENDLESS_SPOOL_STATUS`: Get endless spool status 64 | - `ACE_START_DRYING`: Start filament drying 65 | - `ACE_STOP_DRYING`: Stop filament drying 66 | 67 | ### Step 4: Configure saved_variables 68 | 69 | Ensure your `printer.cfg` includes the saved_variables section: 70 | 71 | ```ini 72 | [save_variables] 73 | filename: ~/printer_data/config/saved_variables.cfg 74 | ``` 75 | 76 | ### Step 5: Restart KlipperScreen 77 | 78 | Restart KlipperScreen to load the new panel: 79 | 80 | ```bash 81 | sudo systemctl restart KlipperScreen 82 | ``` 83 | 84 | ## Usage 85 | 86 | ### Main Screen 87 | 88 | The main screen displays: 89 | - **Status Bar**: Shows current ACE status and loaded slot 90 | - **Endless Spool Switch**: Toggle endless spool functionality (top right) 91 | - **Slot Buttons**: Four slots showing material, temperature, and color 92 | - **Settings Buttons**: Configuration gear icons below each slot 93 | - **Control Buttons**: Refresh and Dryer controls at the bottom 94 | 95 | ### Slot Configuration 96 | 97 | Click the settings gear below any slot to configure: 98 | 99 | 1. **Left Panel Controls**: 100 | - Material selection (PLA, ABS, PETG, etc.) 101 | - Color picker with preview 102 | - Temperature setting with keypad input 103 | - Save/Cancel buttons 104 | 105 | 2. **Right Panel**: Dynamic content area showing: 106 | - Material selection grid 107 | - Color picker with RGB sliders and presets 108 | - Temperature keypad input 109 | 110 | ### Loading/Unloading Slots 111 | 112 | - **Click a slot** to load it (if configured) or unload it (if currently loaded) 113 | - **Confirmation dialogs** appear for load/unload operations 114 | - **Visual feedback** shows the currently loaded slot with white background 115 | 116 | ### Endless Spool 117 | 118 | - **Toggle switch** in top-right corner 119 | - **Global control** affects all slots 120 | - **Status synchronization** with ACE driver state 121 | 122 | ## Command Reference 123 | 124 | ### ACE Driver Commands Used 125 | 126 | | Command | Purpose | Response Format | 127 | |---------|---------|-----------------| 128 | | `ACE_QUERY_SLOTS` | Get all slot data | `// [{"material":"PLA","temp":200,"color":[255,255,255],"status":"ready"},...]` | 129 | | `ACE_GET_CURRENT_INDEX` | Get loaded slot | `// 0` (slot 0) or `// -1` (none) | 130 | | `ACE_SET_SLOT INDEX=0 MATERIAL=PLA COLOR=255,255,255 TEMP=200` | Configure slot | Standard gcode response | 131 | | `ACE_CHANGE_TOOL TOOL=0` | Load slot 0 | Standard gcode response | 132 | | `ACE_CHANGE_TOOL TOOL=-1` | Unload current | Standard gcode response | 133 | | `ACE_ENABLE_ENDLESS_SPOOL` | Enable endless spool | Standard gcode response | 134 | | `ACE_DISABLE_ENDLESS_SPOOL` | Disable endless spool | Standard gcode response | 135 | | `ACE_ENDLESS_SPOOL_STATUS` | Get endless spool status | `// - Currently enabled: True/False` | 136 | 137 | ## Troubleshooting 138 | 139 | ### Panel Not Appearing 140 | - Check that `acepro.py` is in the correct `panels/` directory 141 | - Verify `KlipperScreen.conf` menu configuration 142 | - Restart KlipperScreen service 143 | 144 | ### Slot Status Not Updating 145 | - Ensure ACE driver is properly installed 146 | - Check that `saved_variables.cfg` exists and is writable 147 | - Verify `ACE_GET_CURRENT_INDEX` command works in console 148 | 149 | ### Endless Spool Switch Not Working 150 | - Check `ACE_ENDLESS_SPOOL_STATUS` command output format 151 | - Ensure response includes `"// - Currently enabled: True/False"` 152 | 153 | ### Configuration Screen Too Large 154 | - Panel is optimized for 480px height screens 155 | - For smaller screens, consider adjusting spacing values in the code 156 | 157 | ## File Structure 158 | 159 | ``` 160 | KlipperScreen/ 161 | ├── panels/ 162 | │ └── acepro.py # Main panel file 163 | ├── KlipperScreen.conf # Configuration file 164 | └── README.md # This file 165 | ``` 166 | 167 | ## Dependencies 168 | 169 | - **Python 3**: Required for KlipperScreen 170 | - **GTK 3.0**: GUI framework (gi.repository.Gtk) 171 | - **KlipperScreen Framework**: Base panel classes and utilities 172 | 173 | ## Compatibility 174 | 175 | - **KlipperScreen**: Tested with recent versions 176 | - **Screen Resolution**: Optimized for 480px height touchscreens 177 | - **Klipper**: Requires ACEPROSV08 driver installation 178 | 179 | ## Support 180 | 181 | For issues related to: 182 | - **Panel functionality**: Check this repository's issues 183 | - **ACE Driver**: Refer to ACEPROSV08 driver documentation 184 | - **KlipperScreen**: Refer to official KlipperScreen documentation 185 | 186 | ## License 187 | 188 | This panel is provided as-is for use with KlipperScreen and the Anycubic Color Engine Pro system. 189 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | Anycubic ACE Pro Protocol 2 | ========================= 3 | 4 | Transport 5 | ========= 6 | 7 | The ACE Pro talks over USB using a USB CDC device, with no flow control or data 8 | integrity checking. It seems to share a single ringle buffer for input and 9 | output, and sending packets too fast may drop data. Sending packets before 10 | waiting for a response may lose output the printer was sending. The maximum 11 | safe amount to send within a small time span size seems to be 1024 bytes, but 12 | this may change in the future. 13 | 14 | Framing 15 | ======= 16 | 17 | Each JSON command is packed in a frame of the following format: 18 | 19 | - 2 bytes: 0xFF 0xAA 20 | - 2 bytes: Payload length (little endian) 21 | - The JSON itself 22 | - 2 bytes: CRC-16/MCRF4XX code of the JSON (little endian) 23 | - Any number of bytes, ignored for now (do not add) 24 | - 1 byte: 0xFE 25 | 26 | The ACE will disconnect and reconnect if no frame has been completely sent 3 27 | seconds, regardless of whether the frame data has a valid length or CRC. The 28 | keepalive does disregard data from the previous connection: Frames can be split 29 | across multiple connections. 30 | 31 | The header is two bytes, so in the case of one of the bytes getting corrupted 32 | the frame will be ignored, unless the frame contains 0xFF 0xAA in it. If the 33 | header gets corrupted and frame contains 0xFF 0xAA in it the ACE may freeze for 34 | a while trying to read a large frame. You can try and prevent this by 35 | re-generating a payload until the CRC does not match 0xFF 0xAA (little endian). 36 | 37 | If a long frame (greater than 1024 bytes) is requested either by accident or on 38 | purpose, the ACE seems to freeze and enter an unrecoverable state. No amount of 39 | data send to complete the frame's payload unfreezes the machine. 40 | 41 | RPC 42 | === 43 | 44 | **WARNING**: None of this has been tested and may be entirely wrong. 45 | 46 | Each request is sent to the ACE Pro containing the following JSON data: 47 | 48 | - id: The message number 49 | - method: A string identifying the method 50 | - params: Dictionary of method-specific parameters, omit if empty 51 | 52 | Each response is sent from the ACE containing the following JSON data: 53 | 54 | - id: The request's message number 55 | - result: Dictionary of method-specific return data 56 | - code: Method-specific return code 57 | - msg: Method-specific message 58 | 59 | Make sure to lock access to the ACE when sending a request and reading a 60 | response. It's easy to overlook this if you have a background thread that 61 | works to keep the connection alive by sending a command every second. 62 | 63 | Methods 64 | ======= 65 | 66 | This section documents the methods you can call and the data you'll get 67 | back. For values not known, static values are listed. 68 | 69 | This is not a comprehensive API documentation as we don't control the 70 | firmware or have any authority over the ACE or its future updates. 71 | 72 | enable_rfid 73 | ----------- 74 | 75 | Request params: 76 | 77 | - None 78 | 79 | Response data: 80 | - msg: "success" 81 | - code: 0 82 | 83 | Response params: 84 | - None 85 | 86 | disable_rfid 87 | ------------ 88 | 89 | Request params: 90 | 91 | - None 92 | 93 | Response data: 94 | - msg: "success" 95 | - code: 0 96 | 97 | Response params: 98 | - None 99 | 100 | get_info 101 | -------- 102 | 103 | Request params: 104 | 105 | - None 106 | 107 | Response data: 108 | - msg: "success" 109 | - code: 0 110 | 111 | Response params: 112 | 113 | - id: 0 114 | - slots: 4 115 | - model: "Anycubic Color Engine Pro" 116 | - firmware: "V1.3.82" 117 | - boot_firmware: "V1.0.1" 118 | 119 | get_filament_info 120 | ----------------- 121 | 122 | Request params: 123 | 124 | - index: Filament slot number 125 | 126 | Response data: 127 | - msg: "success" 128 | - code: 0 129 | 130 | Response params: 131 | 132 | - index: Filament slot number 133 | - sku: "ABCDEF-01" 134 | - brand: "FakeBrand" 135 | - type: "PLA", "PLA+", "TPU", "ABS", "PETG", etc 136 | - color: [0, 0, 0] (Red, green, blue decimal values 0-255) 137 | - rfid: 0 (Information not found), 1 (Failed to identify), 2 (Identified), 3 (Identifying) 138 | - extruder_temp: Dictionary of temperature data 139 | - hotbed_temp: Dictionary of temperature data 140 | - diameter: 1.75 (Filament diameter in millimeters) 141 | - total: 330 142 | - current: 0 143 | 144 | Temperature data dictionary: 145 | 146 | - min: Temperature in Celsius, integer 147 | - max: Temperature in Celsius, integer 148 | 149 | get_status 150 | ---------- 151 | 152 | Request params: 153 | 154 | - None 155 | 156 | Response data: 157 | - msg: "success" 158 | - code: 0 159 | 160 | Response params: 161 | 162 | - status: "ready", "busy" 163 | - action: "feeding", "unwinding", "shifting" (changing gears) 164 | - dryer_status: Dictionary of dryer status 165 | - temp: Dryer temperature in Celsius 166 | - enable_rfid: 1 167 | - fan_speed: Fan speed in RPM 168 | - feed_assist_count: 0 169 | - cont_assist_time: 0.0 (Continous feeding time in milliseconds) 170 | - slots: Array of dictionary of slot status 171 | 172 | Dryer status dictionary: 173 | - status: "stop", "drying" 174 | - target_temp: 60 175 | - duration: 300 (Minutes) 176 | - remain_time: 50 (Minutes) 177 | 178 | Slot status dictionary: 179 | - index: Filament slot number 180 | - status: "ready" 181 | - sku: "ABCDEF-01" 182 | - brand: "FakeBrand" 183 | - type: "PLA", "PLA+", "TPU", "ABS", "PETG", etc 184 | - color: [0, 0, 0] (Red, green, blue decimal values 0-255) 185 | - rfid: 0 (Information not found), 1 (Failed to identify), 2 (Identified), 3 (Identifying) 186 | 187 | drying 188 | ------ 189 | 190 | Request params: 191 | 192 | - temp: Dryer temperature in Celsius 193 | - fan_speed: 7000 (RPM) 194 | - duration: 240 (minutes) 195 | 196 | Response data: 197 | - msg: "drying" 198 | - code: 0 199 | 200 | Response params: 201 | - None 202 | 203 | drying_stop 204 | ----------- 205 | 206 | Request params: 207 | 208 | - None 209 | 210 | Response data: 211 | - msg: "success" 212 | - code: 0 213 | 214 | Response params: 215 | - None 216 | 217 | unwind_filament 218 | --------------- 219 | 220 | Request params: 221 | 222 | - index: Filament slot number 223 | - length: 300, 70 224 | - speed: 10, 15 225 | - mode: 0 (normal mode), 1 (enhanced mode) 226 | 227 | Response data: 228 | - msg: "success" 229 | - code: 0 230 | 231 | Response params: 232 | - None 233 | 234 | update_unwinding_speed 235 | ---------------------- 236 | 237 | Request params: 238 | 239 | - index: Filament slot number 240 | - speed: 15 241 | 242 | Response data: 243 | - msg: "success" 244 | - code: 0 245 | 246 | Response params: 247 | - None 248 | 249 | stop_unwind_filament 250 | -------------------- 251 | 252 | Request params: 253 | 254 | - index: Filament slot number 255 | 256 | Response data: 257 | - msg: "success" 258 | - code: 0 259 | 260 | Response params: 261 | - None 262 | 263 | feed_filament 264 | ------------- 265 | 266 | Request params: 267 | 268 | - index: Filament slot number 269 | - length: 2000 270 | - speed: 25 271 | 272 | Response data: 273 | - msg: "success" 274 | - code: 0 275 | 276 | Response params: 277 | - None 278 | 279 | update_feeding_speed 280 | -------------------- 281 | 282 | Request params: 283 | 284 | - index: Filament slot number 285 | - speed: 25 286 | 287 | Response data: 288 | - msg: "success" 289 | - code: 0 290 | 291 | Response params: 292 | - None 293 | 294 | stop_feed_filament 295 | ------------------ 296 | 297 | Request params: 298 | 299 | - index: Filament slot number 300 | 301 | Response data: 302 | - msg: "success" 303 | - code: 0 304 | 305 | Response params: 306 | - None 307 | 308 | start_feed_assist 309 | ----------------- 310 | 311 | Request params: 312 | 313 | - index: Filament slot number 314 | 315 | Response data: 316 | - msg: "success" 317 | - code: 0 318 | 319 | Response params: 320 | - None 321 | 322 | stop_feed_assist 323 | ---------------- 324 | 325 | Request params: 326 | 327 | - index: Filament slot number 328 | 329 | Response data: 330 | - msg: "" 331 | - code: 0 332 | 333 | Response params: 334 | - None 335 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACE PRO SV08 - Anycubic Color Engine Pro Driver for Klipper 2 | 3 | A comprehensive Klipper driver for the Anycubic Color Engine Pro multi-material unit, optimized for SOVOL SV08 and other Klipper-based 3D printers. 4 | 5 | ## 📋 Table of Contents 6 | 7 | - [Features](#-features) 8 | - [Hardware Requirements](#-hardware-requirements) 9 | - [Installation](#-installation) 10 | - [Configuration](#-configuration) 11 | - [Commands Reference](#-commands-reference) 12 | - [Endless Spool Feature](#-endless-spool-feature) 13 | - [Inventory Management](#-inventory-management) 14 | - [Hardware Setup](#-hardware-setup) 15 | - [Contributing](#-contributing) 16 | - [Credits](#-credits) 17 | 18 | Base driver fork: 19 | 20 | https://github.com/BlackFrogKok/BunnyACE 21 | 22 | 23 | - **Multi-Material Support**: Full 4-slot filament management 24 | - **Endless Spool**: Automatic filament switching on runout 25 | - **Persistent State**: Settings and inventory saved across restarts 26 | - **Feed Assist**: Advanced filament feeding control 27 | - **Runout Detection**: Dual sensor runout detection system 28 | - **Inventory Tracking**: Material type, color, and temperature management 29 | - **Debug Tools**: Comprehensive diagnostic commands 30 | - **Seamless Integration**: Native Klipper integration 31 | 32 | ## 🔧 Hardware Requirements 33 | 34 | ### Required Components 35 | - **Anycubic Color Engine Pro** multi-material unit 36 | - **Filament Sensors**: 37 | - Extruder sensor (at splitter exit) 38 | - Toolhead sensor (before hotend) 39 | - **Hotend**: Compatible with filament cutting (recommended) 40 | 41 | ### Recommended Hardware 42 | - **Filament Splitter**: [BAMBULAB filament splitter](https://www.printables.com/model/1133951-v4-toolhead-ideal-for-mmu-for-sv08-and-any-voron-g) 43 | - **Toolhead**: [Nadir extruder for SV08/Voron](https://www.printables.com/model/1133951-v4-toolhead-ideal-for-mmu-for-sv08-and-any-voron-g) 44 | - **Cutting Mod**: [Mr Goodman BAMBULAB hotend with cutter](https://www.printables.com/model/1099177-sovol-sv08-head-filament-cutting-mod) 45 | 46 | ## 📦 Installation 47 | 48 | ### 1. Clone Repository 49 | ```bash 50 | cd ~ 51 | git clone https://github.com/szkrisz/ACEPROSV08.git 52 | ``` 53 | 54 | ### 2. Create Symbolic Links 55 | ```bash 56 | # Link the driver to Klipper extras 57 | ln -sf ~/ACEPROSV08/extras/ace.py ~/klipper/klippy/extras/ace.py 58 | 59 | # Link the configuration file 60 | ln -sf ~/ACEPROSV08/ace.cfg ~/printer_data/config/ace.cfg 61 | ``` 62 | 63 | ### 3. Update Python Dependencies 64 | ```bash 65 | # Activate Klipper virtual environment 66 | source ~/klippy-env/bin/activate 67 | 68 | # Update pyserial to version 4.5 or higher 69 | pip3 install pyserial --upgrade 70 | ``` 71 | 72 | ### 4. Update Printer Configuration 73 | Add to your `printer.cfg`: 74 | ```ini 75 | [include ace.cfg] 76 | ``` 77 | 78 | ## ⚙️ Configuration 79 | 80 | ### Basic Configuration (ace.cfg) 81 | ```ini 82 | [ace] 83 | serial: /dev/ttyACM0 84 | baud: 115200 85 | extruder_sensor_pin: ^PC2 86 | toolhead_sensor_pin: ^PC3 87 | feed_speed: 50 88 | retract_speed: 50 89 | toolchange_retract_length: 150 90 | toolchange_load_length: 630 91 | toolhead_sensor_to_nozzle: 10 92 | endless_spool: True 93 | ``` 94 | 95 | ### Pin Configuration 96 | ![Connector Pinout](/img/connector.png) 97 | 98 | Connect the ACE Pro to a regular USB port and configure the sensor pins according to your board layout. 99 | 100 | ## 🎯 Commands Reference 101 | 102 | ### Basic Operations 103 | | Command | Description | Parameters | 104 | |---------|-------------|------------| 105 | | `ACE_CHANGE_TOOL` | Manual tool change | `TOOL=<0-3\|-1>` | 106 | | `ACE_CHANGE_SPOOL` | Change spool (retract filament back to ACEPRO) | `INDEX=<0-3>` | 107 | | `ACE_FEED` | Feed filament | `INDEX=<0-3> LENGTH= [SPEED=]` | 108 | | `ACE_RETRACT` | Retract filament | `INDEX=<0-3> LENGTH= [SPEED=]` | 109 | | `ACE_GET_CURRENT_INDEX` | Get current slot | Returns: `-1, 0, 1, 2, 3` | 110 | 111 | ### Feed Assist 112 | | Command | Description | Parameters | 113 | |---------|-------------|------------| 114 | | `ACE_ENABLE_FEED_ASSIST` | Enable feed assist | `INDEX=<0-3>` | 115 | | `ACE_DISABLE_FEED_ASSIST` | Disable feed assist | `INDEX=<0-3>` | 116 | 117 | ### Inventory Management 118 | | Command | Description | Parameters | 119 | |---------|-------------|------------| 120 | | `ACE_SET_SLOT` | Set slot info | `INDEX=<0-3> COLOR= MATERIAL= TEMP=<°C>` | 121 | | `ACE_SET_SLOT` | Set slot empty | `INDEX=<0-3> EMPTY=1` | 122 | | `ACE_QUERY_SLOTS` | Get all slots | Returns JSON | 123 | | `ACE_SAVE_INVENTORY` | Save inventory | Manual save trigger | 124 | 125 | ### Endless Spool 126 | | Command | Description | 127 | |---------|-------------| 128 | | `ACE_ENABLE_ENDLESS_SPOOL` | Enable endless spool | 129 | | `ACE_DISABLE_ENDLESS_SPOOL` | Disable endless spool | 130 | | `ACE_ENDLESS_SPOOL_STATUS` | Show endless spool status | 131 | 132 | ### Diagnostics 133 | | Command | Description | 134 | |---------|-------------| 135 | | `ACE_TEST_RUNOUT_SENSOR` | Test sensor states | 136 | | `ACE_DEBUG` | Debug ACE communication | 137 | | `ACE_GET_CURRENT_INDEX` | Get currently loaded slot index | 138 | 139 | ### Dryer Control 140 | | Command | Description | Parameters | 141 | |---------|-------------|------------| 142 | | `ACE_START_DRYING` | Start dryer | `TEMP=<°C> [DURATION=]` | 143 | | `ACE_STOP_DRYING` | Stop dryer | - | 144 | 145 | ## 🔄 Endless Spool Feature 146 | 147 | The endless spool feature automatically switches to the next available filament slot when runout is detected, enabling continuous printing across multiple spools. 148 | 149 | ### How It Works 150 | 1. **Runout Detection** → Immediate response (no delay) 151 | 2. **Disable Feed Assist** → Stop feeding from empty slot 152 | 3. **Switch Filament** → Feed from next available slot 153 | 4. **Enable Feed Assist** → Resume normal operation 154 | 5. **Update State** → Save new slot index 155 | 6. **Continue Printing** → Seamless continuation 156 | 157 | ### Enable/Disable 158 | ```gcode 159 | # Enable endless spool 160 | ACE_ENABLE_ENDLESS_SPOOL 161 | 162 | # Disable endless spool 163 | ACE_DISABLE_ENDLESS_SPOOL 164 | 165 | # Check status 166 | ACE_ENDLESS_SPOOL_STATUS 167 | ``` 168 | 169 | ### Behavior 170 | - **Enabled**: Automatic switching on runout 171 | - **Disabled**: Print pauses on runout (standard behavior) 172 | - **No Available Slots**: Print pauses automatically 173 | 174 | ## 📊 Inventory Management 175 | 176 | Track filament materials, colors, and printing temperatures for each slot. 177 | 178 | ### Set Slot Information 179 | ```gcode 180 | # Set slot with filament 181 | ACE_SET_SLOT INDEX=0 COLOR=255,0,0 MATERIAL=PLA TEMP=210 182 | 183 | # Set slot as empty 184 | ACE_SET_SLOT INDEX=1 EMPTY=1 185 | ``` 186 | 187 | ### Query Inventory 188 | ```gcode 189 | # Get all slots as JSON 190 | ACE_QUERY_SLOTS 191 | 192 | # Example response: 193 | # [ 194 | # {"status": "ready", "color": [255,0,0], "material": "PLA", "temp": 210}, 195 | # {"status": "empty", "color": [0,0,0], "material": "", "temp": 0}, 196 | # {"status": "ready", "color": [0,255,0], "material": "PETG", "temp": 240}, 197 | # {"status": "empty", "color": [0,0,0], "material": "", "temp": 0} 198 | # ] 199 | ``` 200 | 201 | ### Persistent Storage 202 | - Inventory is automatically saved to Klipper's `save_variables` 203 | - Restored on restart 204 | - Manual save: `ACE_SAVE_INVENTORY` 205 | 206 | ## 🔌 Hardware Setup 207 | 208 | ### Sensor Installation 209 | 1. **Extruder Sensor**: Install at the splitter exit point 210 | 2. **Toolhead Sensor**: Install before the hotend entry 211 | 3. **Wiring**: Connect sensors to configured pins with pullup resistors 212 | 213 | ### USB Connection 214 | Connect the ACE Pro unit to your printer's host computer via USB. The driver will automatically detect the device. 215 | 216 | ### Splitter Configuration 217 | Use a BAMBULAB-compatible filament splitter for optimal performance with the ACE Pro system. 218 | 219 | ## 🤝 Contributing 220 | 221 | Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests. 222 | 223 | ### Development Setup 224 | 1. Fork the repository 225 | 2. Create a feature branch 226 | 3. Make your changes 227 | 4. Test thoroughly 228 | 5. Submit a pull request 229 | 230 | ## 📜 Credits 231 | 232 | This project is based on excellent work from: 233 | 234 | - **[ACEResearch](https://github.com/printers-for-people/ACEResearch.git)** - Original ACE Pro research 235 | - **[DuckACE](https://github.com/utkabobr/DuckACE.git)** - Base driver implementation 236 | - **[BunyAce](https://github.com/BlackFrogKok/BunnyACE)** - Base driver fork 237 | 238 | ## 📄 License 239 | 240 | This project is licensed under the same terms as the original projects it's based on. 241 | 242 | --- 243 | 244 | **⚠️ Note**: This is a work-in-progress driver. Please test thoroughly and report any issues you encounter. 245 | 246 | 247 | 248 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The GNU General Public License is a free, copyleft license for 12 | software and other kinds of works. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | the GNU General Public License is intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. We, the Free Software Foundation, use the 19 | GNU General Public License for most of our software; it applies also to 20 | any other work released this way by its authors. You can apply it to 21 | your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | To protect your rights, we need to prevent others from denying you 31 | these rights or asking you to surrender the rights. Therefore, you have 32 | certain responsibilities if you distribute copies of the software, or if 33 | you modify it: responsibilities to respect the freedom of others. 34 | 35 | For example, if you distribute copies of such a program, whether 36 | gratis or for a fee, you must pass on to the recipients the same 37 | freedoms that you received. You must make sure that they, too, receive 38 | or can get the source code. And you must show them these terms so they 39 | know their rights. 40 | 41 | Developers that use the GNU GPL protect your rights with two steps: 42 | (1) assert copyright on the software, and (2) offer you this License 43 | giving you legal permission to copy, distribute and/or modify it. 44 | 45 | For the developers' and authors' protection, the GPL clearly explains 46 | that there is no warranty for this free software. For both users' and 47 | authors' sake, the GPL requires that modified versions be marked as 48 | changed, so that their problems will not be attributed erroneously to 49 | authors of previous versions. 50 | 51 | Some devices are designed to deny users access to install or run 52 | modified versions of the software inside them, although the manufacturer 53 | can do so. This is fundamentally incompatible with the aim of 54 | protecting users' freedom to change the software. The systematic 55 | pattern of such abuse occurs in the area of products for individuals to 56 | use, which is precisely where it is most unacceptable. Therefore, we 57 | have designed this version of the GPL to prohibit the practice for those 58 | products. If such problems arise substantially in other domains, we 59 | stand ready to extend this provision to those domains in future versions 60 | of the GPL, as needed to protect the freedom of users. 61 | 62 | Finally, every program is threatened constantly by software patents. 63 | States should not allow patents to restrict development and use of 64 | software on general-purpose computers, but in those that do, we wish to 65 | avoid the special danger that patents applied to a free program could 66 | make it effectively proprietary. To prevent this, the GPL assures that 67 | patents cannot be used to render the program non-free. 68 | 69 | The precise terms and conditions for copying, distribution and 70 | modification follow. 71 | 72 | TERMS AND CONDITIONS 73 | 74 | 0. Definitions. 75 | 76 | "This License" refers to version 3 of the GNU General Public License. 77 | 78 | "Copyright" also means copyright-like laws that apply to other kinds of 79 | works, such as semiconductor masks. 80 | 81 | "The Program" refers to any copyrightable work licensed under this 82 | License. Each licensee is addressed as "you". "Licensees" and 83 | "recipients" may be individuals or organizations. 84 | 85 | To "modify" a work means to copy from or adapt all or part of the work 86 | in a fashion requiring copyright permission, other than the making of an 87 | exact copy. The resulting work is called a "modified version" of the 88 | earlier work or a work "based on" the earlier work. 89 | 90 | A "covered work" means either the unmodified Program or a work based 91 | on the Program. 92 | 93 | To "propagate" a work means to do anything with it that, without 94 | permission, would make you directly or secondarily liable for 95 | infringement under applicable copyright law, except executing it on a 96 | computer or modifying a private copy. Propagation includes copying, 97 | distribution (with or without modification), making available to the 98 | public, and in some countries other activities as well. 99 | 100 | To "convey" a work means any kind of propagation that enables other 101 | parties to make or receive copies. Mere interaction with a user through 102 | a computer network, with no transfer of a copy, is not conveying. 103 | 104 | An interactive user interface displays "Appropriate Legal Notices" 105 | to the extent that it includes a convenient and prominently visible 106 | feature that (1) displays an appropriate copyright notice, and (2) 107 | tells the user that there is no warranty for the work (except to the 108 | extent that warranties are provided), that licensees may convey the 109 | work under this License, and how to view a copy of this License. If 110 | the interface presents a list of user commands or options, such as a 111 | menu, a prominent item in the list meets this criterion. 112 | 113 | 1. Source Code. 114 | 115 | The "source code" for a work means the preferred form of the work 116 | for making modifications to it. "Object code" means any non-source 117 | form of a work. 118 | 119 | A "Standard Interface" means an interface that either is an official 120 | standard defined by a recognized standards body, or, in the case of 121 | interfaces specified for a particular programming language, one that 122 | is widely used among developers working in that language. 123 | 124 | The "System Libraries" of an executable work include anything, other 125 | than the work as a whole, that (a) is included in the normal form of 126 | packaging a Major Component, but which is not part of that Major 127 | Component, and (b) serves only to enable use of the work with that 128 | Major Component, or to implement a Standard Interface for which an 129 | implementation is available to the public in source code form. A 130 | "Major Component", in this context, means a major essential component 131 | (kernel, window system, and so on) of the specific operating system 132 | (if any) on which the executable work runs, or a compiler used to 133 | produce the work, or an object code interpreter used to run it. 134 | 135 | The "Corresponding Source" for a work in object code form means all 136 | the source code needed to generate, install, and (for an executable 137 | work) run the object code and to modify the work, including scripts to 138 | control those activities. However, it does not include the work's 139 | System Libraries, or general-purpose tools or generally available free 140 | programs which are used unmodified in performing those activities but 141 | which are not part of the work. For example, Corresponding Source 142 | includes interface definition files associated with source files for 143 | the work, and the source code for shared libraries and dynamically 144 | linked subprograms that the work is specifically designed to require, 145 | such as by intimate data communication or control flow between those 146 | subprograms and other parts of the work. 147 | 148 | The Corresponding Source need not include anything that users 149 | can regenerate automatically from other parts of the Corresponding 150 | Source. 151 | 152 | The Corresponding Source for a work in source code form is that 153 | same work. 154 | 155 | 2. Basic Permissions. 156 | 157 | All rights granted under this License are granted for the term of 158 | copyright on the Program, and are irrevocable provided the stated 159 | conditions are met. This License explicitly affirms your unlimited 160 | permission to run the unmodified Program. The output from running a 161 | covered work is covered by this License only if the output, given its 162 | content, constitutes a covered work. This License acknowledges your 163 | rights of fair use or other equivalent, as provided by copyright law. 164 | 165 | You may make, run and propagate covered works that you do not 166 | convey, without conditions so long as your license otherwise remains 167 | in force. You may convey covered works to others for the sole purpose 168 | of having them make modifications exclusively for you, or provide you 169 | with facilities for running those works, provided that you comply with 170 | the terms of this License in conveying all material for which you do 171 | not control copyright. Those thus making or running the covered works 172 | for you must do so exclusively on your behalf, under your direction 173 | and control, on terms that prohibit them from making any copies of 174 | your copyrighted material outside their relationship with you. 175 | 176 | Conveying under any other circumstances is permitted solely under 177 | the conditions stated below. Sublicensing is not allowed; section 10 178 | makes it unnecessary. 179 | 180 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 181 | 182 | No covered work shall be deemed part of an effective technological 183 | measure under any applicable law fulfilling obligations under article 184 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 185 | similar laws prohibiting or restricting circumvention of such 186 | measures. 187 | 188 | When you convey a covered work, you waive any legal power to forbid 189 | circumvention of technological measures to the extent such circumvention 190 | is effected by exercising rights under this License with respect to 191 | the covered work, and you disclaim any intention to limit operation or 192 | modification of the work as a means of enforcing, against the work's 193 | users, your or third parties' legal rights to forbid circumvention of 194 | technological measures. 195 | 196 | 4. Conveying Verbatim Copies. 197 | 198 | You may convey verbatim copies of the Program's source code as you 199 | receive it, in any medium, provided that you conspicuously and 200 | appropriately publish on each copy an appropriate copyright notice; 201 | keep intact all notices stating that this License and any 202 | non-permissive terms added in accord with section 7 apply to the code; 203 | keep intact all notices of the absence of any warranty; and give all 204 | recipients a copy of this License along with the Program. 205 | 206 | You may charge any price or no price for each copy that you convey, 207 | and you may offer support or warranty protection for a fee. 208 | 209 | 5. Conveying Modified Source Versions. 210 | 211 | You may convey a work based on the Program, or the modifications to 212 | produce it from the Program, in the form of source code under the 213 | terms of section 4, provided that you also meet all of these conditions: 214 | 215 | a) The work must carry prominent notices stating that you modified 216 | it, and giving a relevant date. 217 | 218 | b) The work must carry prominent notices stating that it is 219 | released under this License and any conditions added under section 220 | 7. This requirement modifies the requirement in section 4 to 221 | "keep intact all notices". 222 | 223 | c) You must license the entire work, as a whole, under this 224 | License to anyone who comes into possession of a copy. This 225 | License will therefore apply, along with any applicable section 7 226 | additional terms, to the whole of the work, and all its parts, 227 | regardless of how they are packaged. This License gives no 228 | permission to license the work in any other way, but it does not 229 | invalidate such permission if you have separately received it. 230 | 231 | d) If the work has interactive user interfaces, each must display 232 | Appropriate Legal Notices; however, if the Program has interactive 233 | interfaces that do not display Appropriate Legal Notices, your 234 | work need not make them do so. 235 | 236 | A compilation of a covered work with other separate and independent 237 | works, which are not by their nature extensions of the covered work, 238 | and which are not combined with it such as to form a larger program, 239 | in or on a volume of a storage or distribution medium, is called an 240 | "aggregate" if the compilation and its resulting copyright are not 241 | used to limit the access or legal rights of the compilation's users 242 | beyond what the individual works permit. Inclusion of a covered work 243 | in an aggregate does not cause this License to apply to the other 244 | parts of the aggregate. 245 | 246 | 6. Conveying Non-Source Forms. 247 | 248 | You may convey a covered work in object code form under the terms 249 | of sections 4 and 5, provided that you also convey the 250 | machine-readable Corresponding Source under the terms of this License, 251 | in one of these ways: 252 | 253 | a) Convey the object code in, or embodied in, a physical product 254 | (including a physical distribution medium), accompanied by the 255 | Corresponding Source fixed on a durable physical medium 256 | customarily used for software interchange. 257 | 258 | b) Convey the object code in, or embodied in, a physical product 259 | (including a physical distribution medium), accompanied by a 260 | written offer, valid for at least three years and valid for as 261 | long as you offer spare parts or customer support for that product 262 | model, to give anyone who possesses the object code either (1) a 263 | copy of the Corresponding Source for all the software in the 264 | product that is covered by this License, on a durable physical 265 | medium customarily used for software interchange, for a price no 266 | more than your reasonable cost of physically performing this 267 | conveying of source, or (2) access to copy the 268 | Corresponding Source from a network server at no charge. 269 | 270 | c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | 276 | d) Convey the object code by offering access from a designated 277 | place (gratis or for a charge), and offer equivalent access to the 278 | Corresponding Source in the same way through the same place at no 279 | further charge. You need not require recipients to copy the 280 | Corresponding Source along with the object code. If the place to 281 | copy the object code is a network server, the Corresponding Source 282 | may be on a different server (operated by you or a third party) 283 | that supports equivalent copying facilities, provided you maintain 284 | clear directions next to the object code saying where to find the 285 | Corresponding Source. Regardless of what server hosts the 286 | Corresponding Source, you remain obligated to ensure that it is 287 | available for as long as needed to satisfy these requirements. 288 | 289 | e) Convey the object code using peer-to-peer transmission, provided 290 | you inform other peers where the object code and Corresponding 291 | Source of the work are being offered to the general public at no 292 | charge under subsection 6d. 293 | 294 | A separable portion of the object code, whose source code is excluded 295 | from the Corresponding Source as a System Library, need not be 296 | included in conveying the object code work. 297 | 298 | A "User Product" is either (1) a "consumer product", which means any 299 | tangible personal property which is normally used for personal, family, 300 | or household purposes, or (2) anything designed or sold for incorporation 301 | into a dwelling. In determining whether a product is a consumer product, 302 | doubtful cases shall be resolved in favor of coverage. For a particular 303 | product received by a particular user, "normally used" refers to a 304 | typical or common use of that class of product, regardless of the status 305 | of the particular user or of the way in which the particular user 306 | actually uses, or expects or is expected to use, the product. A product 307 | is a consumer product regardless of whether the product has substantial 308 | commercial, industrial or non-consumer uses, unless such uses represent 309 | the only significant mode of use of the product. 310 | 311 | "Installation Information" for a User Product means any methods, 312 | procedures, authorization keys, or other information required to install 313 | and execute modified versions of a covered work in that User Product from 314 | a modified version of its Corresponding Source. The information must 315 | suffice to ensure that the continued functioning of the modified object 316 | code is in no case prevented or interfered with solely because 317 | modification has been made. 318 | 319 | If you convey an object code work under this section in, or with, or 320 | specifically for use in, a User Product, and the conveying occurs as 321 | part of a transaction in which the right of possession and use of the 322 | User Product is transferred to the recipient in perpetuity or for a 323 | fixed term (regardless of how the transaction is characterized), the 324 | Corresponding Source conveyed under this section must be accompanied 325 | by the Installation Information. But this requirement does not apply 326 | if neither you nor any third party retains the ability to install 327 | modified object code on the User Product (for example, the work has 328 | been installed in ROM). 329 | 330 | The requirement to provide Installation Information does not include a 331 | requirement to continue to provide support service, warranty, or updates 332 | for a work that has been modified or installed by the recipient, or for 333 | the User Product in which it has been modified or installed. Access to a 334 | network may be denied when the modification itself materially and 335 | adversely affects the operation of the network or violates the rules and 336 | protocols for communication across the network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders of 364 | that material) supplement the terms of this License with terms: 365 | 366 | a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | 369 | b) Requiring preservation of specified reasonable legal notices or 370 | author attributions in that material or in the Appropriate Legal 371 | Notices displayed by works containing it; or 372 | 373 | c) Prohibiting misrepresentation of the origin of that material, or 374 | requiring that modified versions of such material be marked in 375 | reasonable ways as different from the original version; or 376 | 377 | d) Limiting the use for publicity purposes of names of licensors or 378 | authors of the material; or 379 | 380 | e) Declining to grant rights under trademark law for use of some 381 | trade names, trademarks, or service marks; or 382 | 383 | f) Requiring indemnification of licensors and authors of that 384 | material by anyone who conveys the material (or modified versions of 385 | it) with contractual assumptions of liability to the recipient, for 386 | any liability that these contractual assumptions directly impose on 387 | those licensors and authors. 388 | 389 | All other non-permissive additional terms are considered "further 390 | restrictions" within the meaning of section 10. If the Program as you 391 | received it, or any part of it, contains a notice stating that it is 392 | governed by this License along with a term that is a further 393 | restriction, you may remove that term. If a license document contains 394 | a further restriction but permits relicensing or conveying under this 395 | License, you may add to a covered work material governed by the terms 396 | of that license document, provided that the further restriction does 397 | not survive such relicensing or conveying. 398 | 399 | If you add terms to a covered work in accord with this section, you 400 | must place, in the relevant source files, a statement of the 401 | additional terms that apply to those files, or a notice indicating 402 | where to find the applicable terms. 403 | 404 | Additional terms, permissive or non-permissive, may be stated in the 405 | form of a separately written license, or stated as exceptions; 406 | the above requirements apply either way. 407 | 408 | 8. Termination. 409 | 410 | You may not propagate or modify a covered work except as expressly 411 | provided under this License. Any attempt otherwise to propagate or 412 | modify it is void, and will automatically terminate your rights under 413 | this License (including any patent licenses granted under the third 414 | paragraph of section 11). 415 | 416 | However, if you cease all violation of this License, then your 417 | license from a particular copyright holder is reinstated (a) 418 | provisionally, unless and until the copyright holder explicitly and 419 | finally terminates your license, and (b) permanently, if the copyright 420 | holder fails to notify you of the violation by some reasonable means 421 | prior to 60 days after the cessation. 422 | 423 | Moreover, your license from a particular copyright holder is 424 | reinstated permanently if the copyright holder notifies you of the 425 | violation by some reasonable means, this is the first time you have 426 | received notice of violation of this License (for any work) from that 427 | copyright holder, and you cure the violation prior to 30 days after 428 | your receipt of the notice. 429 | 430 | Termination of your rights under this section does not terminate the 431 | licenses of parties who have received copies or rights from you under 432 | this License. If your rights have been terminated and not permanently 433 | reinstated, you do not qualify to receive new licenses for the same 434 | material under section 10. 435 | 436 | 9. Acceptance Not Required for Having Copies. 437 | 438 | You are not required to accept this License in order to receive or 439 | run a copy of the Program. Ancillary propagation of a covered work 440 | occurring solely as a consequence of using peer-to-peer transmission 441 | to receive a copy likewise does not require acceptance. However, 442 | nothing other than this License grants you permission to propagate or 443 | modify any covered work. These actions infringe copyright if you do 444 | not accept this License. Therefore, by modifying or propagating a 445 | covered work, you indicate your acceptance of this License to do so. 446 | 447 | 10. Automatic Licensing of Downstream Recipients. 448 | 449 | Each time you convey a covered work, the recipient automatically 450 | receives a license from the original licensors, to run, modify and 451 | propagate that work, subject to this License. You are not responsible 452 | for enforcing compliance by third parties with this License. 453 | 454 | An "entity transaction" is a transaction transferring control of an 455 | organization, or substantially all assets of one, or subdividing an 456 | organization, or merging organizations. If propagation of a covered 457 | work results from an entity transaction, each party to that 458 | transaction who receives a copy of the work also receives whatever 459 | licenses to the work the party's predecessor in interest had or could 460 | give under the previous paragraph, plus a right to possession of the 461 | Corresponding Source of the work from the predecessor in interest, if 462 | the predecessor has it or can get it with reasonable efforts. 463 | 464 | You may not impose any further restrictions on the exercise of the 465 | rights granted or affirmed under this License. For example, you may 466 | not impose a license fee, royalty, or other charge for exercise of 467 | rights granted under this License, and you may not initiate litigation 468 | (including a cross-claim or counterclaim in a lawsuit) alleging that 469 | any patent claim is infringed by making, using, selling, offering for 470 | sale, or importing the Program or any portion of it. 471 | 472 | 11. Patents. 473 | 474 | A "contributor" is a copyright holder who authorizes use under this 475 | License of the Program or a work on which the Program is based. The 476 | work thus licensed is called the contributor's "contributor version". 477 | 478 | A contributor's "essential patent claims" are all patent claims 479 | owned or controlled by the contributor, whether already acquired or 480 | hereafter acquired, that would be infringed by some manner, permitted 481 | by this License, of making, using, or selling its contributor version, 482 | but do not include claims that would be infringed only as a 483 | consequence of further modification of the contributor version. For 484 | purposes of this definition, "control" includes the right to grant 485 | patent sublicenses in a manner consistent with the requirements of 486 | this License. 487 | 488 | Each contributor grants you a non-exclusive, worldwide, royalty-free 489 | patent license under the contributor's essential patent claims, to 490 | make, use, sell, offer for sale, import and otherwise run, modify and 491 | propagate the contents of its contributor version. 492 | 493 | In the following three paragraphs, a "patent license" is any express 494 | agreement or commitment, however denominated, not to enforce a patent 495 | (such as an express permission to practice a patent or covenant not to 496 | sue for patent infringement). To "grant" such a patent license to a 497 | party means to make such an agreement or commitment not to enforce a 498 | patent against the party. 499 | 500 | If you convey a covered work, knowingly relying on a patent license, 501 | and the Corresponding Source of the work is not available for anyone 502 | to copy, free of charge and under the terms of this License, through a 503 | publicly available network server or other readily accessible means, 504 | then you must either (1) cause the Corresponding Source to be so 505 | available, or (2) arrange to deprive yourself of the benefit of the 506 | patent license for this particular work, or (3) arrange, in a manner 507 | consistent with the requirements of this License, to extend the patent 508 | license to downstream recipients. "Knowingly relying" means you have 509 | actual knowledge that, but for the patent license, your conveying the 510 | covered work in a country, or your recipient's use of the covered work 511 | in a country, would infringe one or more identifiable patents in that 512 | country that you have reason to believe are valid. 513 | 514 | If, pursuant to or in connection with a single transaction or 515 | arrangement, you convey, or propagate by procuring conveyance of, a 516 | covered work, and grant a patent license to some of the parties 517 | receiving the covered work authorizing them to use, propagate, modify 518 | or convey a specific copy of the covered work, then the patent license 519 | you grant is automatically extended to all recipients of the covered 520 | work and works based on it. 521 | 522 | A patent license is "discriminatory" if it does not include within 523 | the scope of its coverage, prohibits the exercise of, or is 524 | conditioned on the non-exercise of one or more of the rights that are 525 | specifically granted under this License. You may not convey a covered 526 | work if you are a party to an arrangement with a third party that is 527 | in the business of distributing software, under which you make payment 528 | to the third party based on the extent of your activity of conveying 529 | the work, and under which the third party grants, to any of the 530 | parties who would receive the covered work from you, a discriminatory 531 | patent license (a) in connection with copies of the covered work 532 | conveyed by you (or copies made from those copies), or (b) primarily 533 | for and in connection with specific products or compilations that 534 | contain the covered work, unless you entered into that arrangement, 535 | or that patent license was granted, prior to 28 March 2007. 536 | 537 | Nothing in this License shall be construed as excluding or limiting 538 | any implied license or other defenses to infringement that may 539 | otherwise be available to you under applicable patent law. 540 | 541 | 12. No Surrender of Others' Freedom. 542 | 543 | If conditions are imposed on you (whether by court order, agreement or 544 | otherwise) that contradict the conditions of this License, they do not 545 | excuse you from the conditions of this License. If you cannot convey a 546 | covered work so as to satisfy simultaneously your obligations under this 547 | License and any other pertinent obligations, then as a consequence you may 548 | not convey it at all. For example, if you agree to terms that obligate you 549 | to collect a royalty for further conveying from those to whom you convey 550 | the Program, the only way you could satisfy both those terms and this 551 | License would be to refrain entirely from conveying the Program. 552 | 553 | 13. Use with the GNU Affero General Public License. 554 | 555 | Notwithstanding any other provision of this License, you have 556 | permission to link or combine any covered work with a work licensed 557 | under version 3 of the GNU Affero General Public License into a single 558 | combined work, and to convey the resulting work. The terms of this 559 | License will continue to apply to the part which is the covered work, 560 | but the special requirements of the GNU Affero General Public License, 561 | section 13, concerning interaction through a network will apply to the 562 | combination as such. 563 | 564 | 14. Revised Versions of this License. 565 | 566 | The Free Software Foundation may publish revised and/or new versions of 567 | the GNU General Public License from time to time. Such new versions will 568 | be similar in spirit to the present version, but may differ in detail to 569 | address new problems or concerns. 570 | 571 | Each version is given a distinguishing version number. If the 572 | Program specifies that a certain numbered version of the GNU General 573 | Public License "or any later version" applies to it, you have the 574 | option of following the terms and conditions either of that numbered 575 | version or of any later version published by the Free Software 576 | Foundation. If the Program does not specify a version number of the 577 | GNU General Public License, you may choose any version ever published 578 | by the Free Software Foundation. 579 | 580 | If the Program specifies that a proxy can decide which future 581 | versions of the GNU General Public License can be used, that proxy's 582 | public statement of acceptance of a version permanently authorizes you 583 | to choose that version for the Program. 584 | 585 | Later license versions may give you additional or different 586 | permissions. However, no additional obligations are imposed on any 587 | author or copyright holder as a result of your choosing to follow a 588 | later version. 589 | 590 | 15. Disclaimer of Warranty. 591 | 592 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 593 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 594 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 595 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 596 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 597 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 598 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 599 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 600 | 601 | 16. Limitation of Liability. 602 | 603 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 604 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 605 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 606 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 607 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 608 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 609 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 610 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 611 | SUCH DAMAGES. 612 | 613 | 17. Interpretation of Sections 15 and 16. 614 | 615 | If the disclaimer of warranty and limitation of liability provided 616 | above cannot be given local legal effect according to their terms, 617 | reviewing courts shall apply local law that most closely approximates 618 | an absolute waiver of all civil liability in connection with the 619 | Program, unless a warranty or assumption of liability accompanies a 620 | copy of the Program in return for a fee. 621 | 622 | END OF TERMS AND CONDITIONS 623 | 624 | How to Apply These Terms to Your New Programs 625 | 626 | If you develop a new program, and you want it to be of the greatest 627 | possible use to the public, the best way to achieve this is to make it 628 | free software which everyone can redistribute and change under these terms. 629 | 630 | To do so, attach the following notices to the program. It is safest 631 | to attach them to the start of each source file to most effectively 632 | state the exclusion of warranty; and each file should have at least 633 | the "copyright" line and a pointer to where the full notice is found. 634 | 635 | 636 | Copyright (C) 637 | 638 | This program is free software: you can redistribute it and/or modify 639 | it under the terms of the GNU General Public License as published by 640 | the Free Software Foundation, either version 3 of the License, or 641 | (at your option) any later version. 642 | 643 | This program is distributed in the hope that it will be useful, 644 | but WITHOUT ANY WARRANTY; without even the implied warranty of 645 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 646 | GNU General Public License for more details. 647 | 648 | You should have received a copy of the GNU General Public License 649 | along with this program. If not, see . 650 | 651 | Also add information on how to contact you by electronic and paper mail. 652 | 653 | If the program does terminal interaction, make it output a short 654 | notice like this when it starts in an interactive mode: 655 | 656 | {project} Copyright (C) {year} {fullname} 657 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 658 | This is free software, and you are welcome to redistribute it 659 | under certain conditions; type `show c' for details. 660 | 661 | The hypothetical commands `show w' and `show c' should show the appropriate 662 | parts of the General Public License. Of course, your program's commands 663 | might be different; for a GUI interface, you would use an "about box". 664 | 665 | You should also get your employer (if you work as a programmer) or school, 666 | if any, to sign a "copyright disclaimer" for the program, if necessary. 667 | For more information on this, and how to apply and follow the GNU GPL, see 668 | . 669 | 670 | The GNU General Public License does not permit incorporating your program 671 | into proprietary programs. If your program is a subroutine library, you 672 | may consider it more useful to permit linking proprietary applications with 673 | the library. If this is what you want to do, use the GNU Lesser General 674 | Public License instead of this License. But first, please read 675 | . 676 | -------------------------------------------------------------------------------- /extras/ace.py: -------------------------------------------------------------------------------- 1 | import serial, threading, time, logging, json, struct, queue, traceback, re 2 | from serial import SerialException 3 | import serial.tools.list_ports 4 | 5 | class BunnyAce: 6 | def __init__(self, config): 7 | self._connected = False 8 | self._serial = None 9 | self.printer = config.get_printer() 10 | self.reactor = self.printer.get_reactor() 11 | self.gcode = self.printer.lookup_object('gcode') 12 | self._name = config.get_name() 13 | self.lock = False 14 | self.send_time = None 15 | self.read_buffer = bytearray() 16 | if self._name.startswith('ace '): 17 | self._name = self._name[4:] 18 | self.variables = self.printer.lookup_object('save_variables').allVariables 19 | 20 | self.serial_name = config.get('serial', '/dev/ttyACM0') 21 | self.baud = config.getint('baud', 115200) 22 | extruder_sensor_pin = config.get('extruder_sensor_pin', None) 23 | toolhead_sensor_pin = config.get('toolhead_sensor_pin', None) 24 | self.feed_speed = config.getint('feed_speed', 50) 25 | self.retract_speed = config.getint('retract_speed', 50) 26 | self.toolchange_retract_length = config.getint('toolchange_retract_length', 150) 27 | self.toolchange_load_length = config.getint('toolchange_load_length', 630) 28 | self.toolhead_sensor_to_nozzle_length = config.getint('toolhead_sensor_to_nozzle', 0) 29 | # self.extruder_to_blade_length = config.getint('extruder_to_blade', None) 30 | self.bowden_tube_length = config.getint('bowden_tube_length', 1000) 31 | 32 | self.max_dryer_temperature = config.getint('max_dryer_temperature', 55) 33 | 34 | # Endless spool configuration - load from persistent variables if available 35 | saved_endless_spool_enabled = self.variables.get('ace_endless_spool_enabled', False) 36 | 37 | self.endless_spool_enabled = config.getboolean('endless_spool', saved_endless_spool_enabled) 38 | self.endless_spool_in_progress = False 39 | self.endless_spool_runout_detected = False 40 | 41 | self._callback_map = {} 42 | self.park_hit_count = 5 43 | self._feed_assist_index = -1 44 | self._request_id = 0 45 | self._last_assist_count = 0 46 | self._assist_hit_count = 0 47 | self._park_in_progress = False 48 | self._park_is_toolchange = False 49 | self._park_previous_tool = -1 50 | self._park_index = -1 51 | self.endstops = {} 52 | 53 | # Default data to prevent exceptions 54 | self._info = { 55 | 'status': 'ready', 56 | 'dryer': { 57 | 'status': 'stop', 58 | 'target_temp': 0, 59 | 'duration': 0, 60 | 'remain_time': 0 61 | }, 62 | 'temp': 0, 63 | 'enable_rfid': 1, 64 | 'fan_speed': 7000, 65 | 'feed_assist_count': 0, 66 | 'cont_assist_time': 0.0, 67 | 'slots': [ 68 | { 69 | 'index': 0, 70 | 'status': 'empty', 71 | 'sku': '', 72 | 'type': '', 73 | 'color': [0, 0, 0] 74 | }, 75 | { 76 | 'index': 1, 77 | 'status': 'empty', 78 | 'sku': '', 79 | 'type': '', 80 | 'color': [0, 0, 0] 81 | }, 82 | { 83 | 'index': 2, 84 | 'status': 'empty', 85 | 'sku': '', 86 | 'type': '', 87 | 'color': [0, 0, 0] 88 | }, 89 | { 90 | 'index': 3, 91 | 'status': 'empty', 92 | 'sku': '', 93 | 'type': '', 94 | 'color': [0, 0, 0] 95 | } 96 | ] 97 | } 98 | 99 | # Add inventory for 4 slots - load from persistent variables if available 100 | saved_inventory = self.variables.get('ace_inventory', None) 101 | if saved_inventory: 102 | self.inventory = saved_inventory 103 | else: 104 | self.inventory = [ 105 | {"status": "empty", "color": [0, 0, 0], "material": "", "temp": 0} for _ in range(4) 106 | ] 107 | # Register inventory commands 108 | self.gcode.register_command( 109 | 'ACE_SET_SLOT', self.cmd_ACE_SET_SLOT, 110 | desc="Set slot inventory: INDEX= COLOR= MATERIAL= TEMP= | Set status to empty with EMPTY=1" 111 | ) 112 | self.gcode.register_command( 113 | 'ACE_QUERY_SLOTS', self.cmd_ACE_QUERY_SLOTS, 114 | desc="Query all slot inventory as JSON" 115 | ) 116 | 117 | self._create_mmu_sensor(config, extruder_sensor_pin, "extruder_sensor") 118 | self._create_mmu_sensor(config, toolhead_sensor_pin, "toolhead_sensor") 119 | self.printer.register_event_handler('klippy:ready', self._handle_ready) 120 | self.printer.register_event_handler('klippy:disconnect', self._handle_disconnect) 121 | self.gcode.register_command( 122 | 'ACE_DEBUG', self.cmd_ACE_DEBUG, 123 | desc='self.cmd_ACE_DEBUG_help') 124 | self.gcode.register_command( 125 | 'ACE_START_DRYING', self.cmd_ACE_START_DRYING, 126 | desc=self.cmd_ACE_START_DRYING_help) 127 | self.gcode.register_command( 128 | 'ACE_STOP_DRYING', self.cmd_ACE_STOP_DRYING, 129 | desc=self.cmd_ACE_STOP_DRYING_help) 130 | self.gcode.register_command( 131 | 'ACE_ENABLE_FEED_ASSIST', self.cmd_ACE_ENABLE_FEED_ASSIST, 132 | desc=self.cmd_ACE_ENABLE_FEED_ASSIST_help) 133 | self.gcode.register_command( 134 | 'ACE_DISABLE_FEED_ASSIST', self.cmd_ACE_DISABLE_FEED_ASSIST, 135 | desc=self.cmd_ACE_DISABLE_FEED_ASSIST_help) 136 | self.gcode.register_command( 137 | 'ACE_FEED', self.cmd_ACE_FEED, 138 | desc=self.cmd_ACE_FEED_help) 139 | self.gcode.register_command( 140 | 'ACE_RETRACT', self.cmd_ACE_RETRACT, 141 | desc=self.cmd_ACE_RETRACT_help) 142 | self.gcode.register_command( 143 | 'ACE_CHANGE_TOOL', self.cmd_ACE_CHANGE_TOOL, 144 | desc=self.cmd_ACE_CHANGE_TOOL_help) 145 | self.gcode.register_command( 146 | 'ACE_ENABLE_ENDLESS_SPOOL', self.cmd_ACE_ENABLE_ENDLESS_SPOOL, 147 | desc=self.cmd_ACE_ENABLE_ENDLESS_SPOOL_help) 148 | self.gcode.register_command( 149 | 'ACE_DISABLE_ENDLESS_SPOOL', self.cmd_ACE_DISABLE_ENDLESS_SPOOL, 150 | desc=self.cmd_ACE_DISABLE_ENDLESS_SPOOL_help) 151 | self.gcode.register_command( 152 | 'ACE_ENDLESS_SPOOL_STATUS', self.cmd_ACE_ENDLESS_SPOOL_STATUS, 153 | desc=self.cmd_ACE_ENDLESS_SPOOL_STATUS_help) 154 | self.gcode.register_command( 155 | 'ACE_SAVE_INVENTORY', self.cmd_ACE_SAVE_INVENTORY, 156 | desc=self.cmd_ACE_SAVE_INVENTORY_help) 157 | self.gcode.register_command( 158 | 'ACE_TEST_RUNOUT_SENSOR', self.cmd_ACE_TEST_RUNOUT_SENSOR, 159 | desc=self.cmd_ACE_TEST_RUNOUT_SENSOR_help) 160 | self.gcode.register_command( 161 | 'ACE_CHANGE_SPOOL', self.cmd_ACE_CHANGE_SPOOL, 162 | desc=self.cmd_ACE_CHANGE_SPOOL_help) 163 | self.gcode.register_command( 164 | 'ACE_GET_CURRENT_INDEX', self.cmd_ACE_GET_CURRENT_INDEX, 165 | desc=self.cmd_ACE_GET_CURRENT_INDEX_help) 166 | 167 | 168 | def _calc_crc(self, buffer): 169 | _crc = 0xffff 170 | for byte in buffer: 171 | data = byte 172 | data ^= _crc & 0xff 173 | data ^= (data & 0x0f) << 4 174 | _crc = ((data << 8) | (_crc >> 8)) ^ (data >> 4) ^ (data << 3) 175 | return _crc 176 | 177 | def _send_request(self, request): 178 | if not 'id' in request: 179 | request['id'] = self._request_id 180 | self._request_id += 1 181 | 182 | payload = json.dumps(request) 183 | payload = bytes(payload, 'utf-8') 184 | 185 | data = bytes([0xFF, 0xAA]) 186 | data += struct.pack('@H', len(payload)) 187 | data += payload 188 | data += struct.pack('@H', self._calc_crc(payload)) 189 | data += bytes([0xFE]) 190 | self._serial.write(data) 191 | 192 | 193 | def _reader(self, eventtime): 194 | 195 | if self.lock and (self.reactor.monotonic() - self.send_time) > 2: 196 | self.lock = False 197 | self.read_buffer = bytearray() 198 | self.gcode.respond_info(f"timeout {self.reactor.monotonic()}") 199 | 200 | buffer = bytearray() 201 | try: 202 | raw_bytes = self._serial.read(size=4096) 203 | except SerialException: 204 | self.gcode.respond_info("Unable to communicate with the ACE PRO" + traceback.format_exc()) 205 | self.lock = False 206 | self.gcode.respond_info('Try reconnecting') 207 | self._serial_disconnect() 208 | self.connect_timer = self.reactor.register_timer(self._connect, self.reactor.NOW) 209 | return self.reactor.NEVER 210 | 211 | if len(raw_bytes): 212 | text_buffer = self.read_buffer + raw_bytes 213 | i = text_buffer.find(b'\xfe') 214 | if i >= 0: 215 | buffer = text_buffer 216 | self.read_buffer = bytearray() 217 | else: 218 | self.read_buffer += raw_bytes 219 | return eventtime + 0.1 220 | 221 | else: 222 | return eventtime + 0.1 223 | 224 | if len(buffer) < 7: 225 | return eventtime + 0.1 226 | 227 | if buffer[0:2] != bytes([0xFF, 0xAA]): 228 | self.lock = False 229 | self.gcode.respond_info("Invalid data from ACE PRO (head bytes)") 230 | self.gcode.respond_info(str(buffer)) 231 | return eventtime + 0.1 232 | 233 | payload_len = struct.unpack('= 0: 386 | self._endless_spool_runout_handler() 387 | 388 | # Adjust monitoring frequency based on state 389 | if is_printing: 390 | return eventtime + 0.05 # Check every 50ms during printing 391 | else: 392 | return eventtime + 0.2 # Check every 200ms when idle 393 | 394 | except Exception as e: 395 | logging.info(f'ACE: Endless spool monitor error: {str(e)}') 396 | return eventtime + 0.1 397 | 398 | def _on_toolhead_move(self, print_time, newpos, oldpos): 399 | """Monitor toolhead moves for extruder movement during printing - removed distance tracking""" 400 | # This method is kept for potential future use but distance tracking removed 401 | pass 402 | 403 | def _create_mmu_sensor(self, config, pin, name): 404 | section = "filament_switch_sensor %s" % name 405 | config.fileconfig.add_section(section) 406 | config.fileconfig.set(section, "switch_pin", pin) 407 | config.fileconfig.set(section, "pause_on_runout", "False") 408 | fs = self.printer.load_object(config, section) 409 | 410 | ppins = self.printer.lookup_object('pins') 411 | pin_params = ppins.parse_pin(pin, True, True) 412 | share_name = "%s:%s" % (pin_params['chip_name'], pin_params['pin']) 413 | ppins.allow_multi_use_pin(share_name) 414 | mcu_endstop = ppins.setup_pin('endstop', pin) 415 | 416 | query_endstops = self.printer.load_object(config, "query_endstops") 417 | query_endstops.register_endstop(mcu_endstop, share_name) 418 | self.endstops[name] = mcu_endstop 419 | 420 | def _check_endstop_state(self, name): 421 | print_time = self.toolhead.get_last_move_time() 422 | return bool(self.endstops[name].query_endstop(print_time)) 423 | 424 | def _serial_disconnect(self): 425 | 426 | if self._serial is not None and self._serial.isOpen(): 427 | self._serial.close() 428 | self._connected = False 429 | 430 | self.reactor.unregister_timer(self.reader_timer) 431 | self.reactor.unregister_timer(self.writer_timer) 432 | 433 | def _connect(self, eventtime): 434 | 435 | try: 436 | port = self.find_com_port('ACE') 437 | if port is None: 438 | return eventtime + 1 439 | self.gcode.respond_info('Try connecting') 440 | self._serial = serial.Serial( 441 | port=port, 442 | baudrate=self.baud, 443 | timeout=0, 444 | write_timeout=0) 445 | 446 | if self._serial.isOpen(): 447 | self._connected = True 448 | logging.info('ACE: Connected to ' + port) 449 | self.gcode.respond_info(f'ACE: Connected to {port} {eventtime}') 450 | self.writer_timer = self.reactor.register_timer(self._writer, self.reactor.NOW) 451 | self.reader_timer = self.reactor.register_timer(self._reader, self.reactor.NOW) 452 | self.send_request(request={"method": "get_info"}, 453 | callback=lambda self, response: self.gcode.respond_info(str(response))) 454 | # --- Added: Check ace_current_index and enable feed assist if needed --- 455 | ace_current_index = self.variables.get('ace_current_index', -1) 456 | if ace_current_index != -1: 457 | self.gcode.respond_info(f'ACE: Re-enabling feed assist on reconnect for index {ace_current_index}') 458 | self._enable_feed_assist(ace_current_index) 459 | # --------------------------------------------------------------- 460 | self.reactor.unregister_timer(self.connect_timer) 461 | return self.reactor.NEVER 462 | except serial.serialutil.SerialException: 463 | self._serial = None 464 | return eventtime + 1 465 | 466 | 467 | cmd_ACE_START_DRYING_help = 'Starts ACE Pro dryer' 468 | 469 | def cmd_ACE_START_DRYING(self, gcmd): 470 | temperature = gcmd.get_int('TEMP') 471 | duration = gcmd.get_int('DURATION', 240) 472 | 473 | if duration <= 0: 474 | raise gcmd.error('Wrong duration') 475 | if temperature <= 0 or temperature > self.max_dryer_temperature: 476 | raise gcmd.error('Wrong temperature') 477 | 478 | def callback(self, response): 479 | if 'code' in response and response['code'] != 0: 480 | raise gcmd.error("ACE Error: " + response['msg']) 481 | 482 | self.gcode.respond_info('Started ACE drying') 483 | 484 | self.send_request( 485 | request={"method": "drying", "params": {"temp": temperature, "fan_speed": 7000, "duration": duration}}, 486 | callback=callback) 487 | 488 | cmd_ACE_STOP_DRYING_help = 'Stops ACE Pro dryer' 489 | 490 | def cmd_ACE_STOP_DRYING(self, gcmd): 491 | def callback(self, response): 492 | if 'code' in response and response['code'] != 0: 493 | raise gcmd.error("ACE Error: " + response['msg']) 494 | 495 | self.gcode.respond_info('Stopped ACE drying') 496 | 497 | self.send_request(request={"method": "drying_stop"}, callback=callback) 498 | 499 | def _enable_feed_assist(self, index): 500 | def callback(self, response): 501 | if 'code' in response and response['code'] != 0: 502 | raise ValueError("ACE Error: " + response['msg']) 503 | else: 504 | self._feed_assist_index = index 505 | self.gcode.respond_info(str(response)) 506 | 507 | self.send_request(request={"method": "start_feed_assist", "params": {"index": index}}, callback=callback) 508 | self.dwell(delay=0.7) 509 | 510 | cmd_ACE_ENABLE_FEED_ASSIST_help = 'Enables ACE feed assist' 511 | 512 | def cmd_ACE_ENABLE_FEED_ASSIST(self, gcmd): 513 | index = gcmd.get_int('INDEX') 514 | 515 | if index < 0 or index >= 4: 516 | raise gcmd.error('Wrong index') 517 | 518 | self._enable_feed_assist(index) 519 | 520 | def _disable_feed_assist(self, index): 521 | def callback(self, response): 522 | if 'code' in response and response['code'] != 0: 523 | raise ValueError("ACE Error: " + response['msg']) 524 | 525 | self._feed_assist_index = -1 526 | self.gcode.respond_info('Disabled ACE feed assist') 527 | 528 | self.send_request(request={"method": "stop_feed_assist", "params": {"index": index}}, callback=callback) 529 | self.dwell(0.3) 530 | 531 | cmd_ACE_DISABLE_FEED_ASSIST_help = 'Disables ACE feed assist' 532 | 533 | def cmd_ACE_DISABLE_FEED_ASSIST(self, gcmd): 534 | if self._feed_assist_index != -1: 535 | index = gcmd.get_int('INDEX', self._feed_assist_index) 536 | else: 537 | index = gcmd.get_int('INDEX') 538 | 539 | if index < 0 or index >= 4: 540 | raise gcmd.error('Wrong index') 541 | 542 | self._disable_feed_assist(index) 543 | 544 | def _feed(self, index, length, speed): 545 | def callback(self, response): 546 | if 'code' in response and response['code'] != 0: 547 | raise ValueError("ACE Error: " + response['msg']) 548 | 549 | self.send_request( 550 | request={"method": "feed_filament", "params": {"index": index, "length": length, "speed": speed}}, 551 | callback=callback) 552 | self.dwell(delay=(length / speed) + 0.1) 553 | 554 | cmd_ACE_FEED_help = 'Feeds filament from ACE' 555 | 556 | def cmd_ACE_FEED(self, gcmd): 557 | index = gcmd.get_int('INDEX') 558 | length = gcmd.get_int('LENGTH') 559 | speed = gcmd.get_int('SPEED', self.feed_speed) 560 | 561 | if index < 0 or index >= 4: 562 | raise gcmd.error('Wrong index') 563 | if length <= 0: 564 | raise gcmd.error('Wrong length') 565 | if speed <= 0: 566 | raise gcmd.error('Wrong speed') 567 | 568 | self._feed(index, length, speed) 569 | 570 | def _retract(self, index, length, speed): 571 | def callback(self, response): 572 | if 'code' in response and response['code'] != 0: 573 | raise ValueError("ACE Error: " + response['msg']) 574 | 575 | self.send_request( 576 | request={"method": "unwind_filament", "params": {"index": index, "length": length, "speed": speed}}, 577 | callback=callback) 578 | self.dwell(delay=(length / speed) + 0.1) 579 | 580 | cmd_ACE_RETRACT_help = 'Retracts filament back to ACE' 581 | 582 | def cmd_ACE_RETRACT(self, gcmd): 583 | index = gcmd.get_int('INDEX') 584 | length = gcmd.get_int('LENGTH') 585 | speed = gcmd.get_int('SPEED', self.retract_speed) 586 | 587 | if index < 0 or index >= 4: 588 | raise gcmd.error('Wrong index') 589 | if length <= 0: 590 | raise gcmd.error('Wrong length') 591 | if speed <= 0: 592 | raise gcmd.error('Wrong speed') 593 | 594 | self._retract(index, length, speed) 595 | 596 | def _park_to_toolhead(self, tool): 597 | 598 | sensor_extruder = self.printer.lookup_object("filament_switch_sensor %s" % "extruder_sensor", None) 599 | 600 | self.wait_ace_ready() 601 | 602 | self._feed(tool, self.toolchange_load_length, self.retract_speed) 603 | self.variables['ace_filament_pos'] = "bowden" 604 | 605 | self.wait_ace_ready() 606 | 607 | self._enable_feed_assist(tool) 608 | 609 | while not bool(sensor_extruder.runout_helper.filament_present): 610 | self.dwell(delay=0.1) 611 | 612 | if not bool(sensor_extruder.runout_helper.filament_present): 613 | raise ValueError("Filament stuck " + str(bool(sensor_extruder.runout_helper.filament_present))) 614 | else: 615 | self.variables['ace_filament_pos'] = "spliter" 616 | 617 | while not self._check_endstop_state('toolhead_sensor'): 618 | self._extruder_move(1, 5) 619 | self.dwell(delay=0.01) 620 | 621 | self.variables['ace_filament_pos'] = "toolhead" 622 | 623 | self._extruder_move(self.toolhead_sensor_to_nozzle_length, 5) 624 | self.variables['ace_filament_pos'] = "nozzle" 625 | 626 | cmd_ACE_CHANGE_TOOL_help = 'Changes tool' 627 | 628 | def cmd_ACE_CHANGE_TOOL(self, gcmd): 629 | tool = gcmd.get_int('TOOL') 630 | sensor_extruder = self.printer.lookup_object("filament_switch_sensor %s" % "extruder_sensor", None) 631 | 632 | if tool < -1 or tool >= 4: 633 | raise gcmd.error('Wrong tool') 634 | 635 | was = self.variables.get('ace_current_index', -1) 636 | if was == tool: 637 | gcmd.respond_info('ACE: Not changing tool, current index already ' + str(tool)) 638 | self._enable_feed_assist(tool) 639 | return 640 | 641 | if tool != -1: 642 | status = self._info['slots'][tool]['status'] 643 | if status != 'ready': 644 | self.gcode.run_script_from_command('_ACE_ON_EMPTY_ERROR INDEX=' + str(tool)) 645 | return 646 | 647 | # Temporarily disable endless spool during manual toolchange 648 | endless_spool_was_enabled = self.endless_spool_enabled 649 | if endless_spool_was_enabled: 650 | self.endless_spool_enabled = False 651 | self.endless_spool_runout_detected = False 652 | self._park_in_progress = True 653 | self.gcode.run_script_from_command('_ACE_PRE_TOOLCHANGE FROM=' + str(was) + ' TO=' + str(tool)) 654 | 655 | logging.info('ACE: Toolchange ' + str(was) + ' => ' + str(tool)) 656 | if was != -1: 657 | self._disable_feed_assist(was) 658 | self.wait_ace_ready() 659 | if self.variables.get('ace_filament_pos', "spliter") == "nozzle": 660 | self.gcode.run_script_from_command('CUT_TIP') 661 | self.variables['ace_filament_pos'] = "toolhead" 662 | 663 | if self.variables.get('ace_filament_pos', "spliter") == "toolhead": 664 | while bool(sensor_extruder.runout_helper.filament_present): 665 | self._extruder_move(-50, 10) 666 | self._retract(was, 100, self.retract_speed) 667 | self.wait_ace_ready() 668 | self.variables['ace_filament_pos'] = "bowden" 669 | 670 | self.wait_ace_ready() 671 | 672 | self._retract(was, self.toolchange_retract_length, self.retract_speed) 673 | self.wait_ace_ready() 674 | self.variables['ace_filament_pos'] = "spliter" 675 | 676 | if tool != -1: 677 | self._park_to_toolhead(tool) 678 | else: 679 | self._park_to_toolhead(tool) 680 | 681 | gcode_move = self.printer.lookup_object('gcode_move') 682 | gcode_move.reset_last_position() 683 | 684 | self.gcode.run_script_from_command('_ACE_POST_TOOLCHANGE FROM=' + str(was) + ' TO=' + str(tool)) 685 | self.variables['ace_current_index'] = tool 686 | gcode_move.reset_last_position() 687 | # Force save to disk 688 | self.gcode.run_script_from_command('SAVE_VARIABLE VARIABLE=ace_current_index VALUE=' + str(tool)) 689 | self.gcode.run_script_from_command( 690 | f"""SAVE_VARIABLE VARIABLE=ace_filament_pos VALUE='"{self.variables['ace_filament_pos']}"'""") 691 | self._park_in_progress = False 692 | 693 | # Re-enable endless spool if it was enabled before 694 | if endless_spool_was_enabled: 695 | self.endless_spool_enabled = True 696 | 697 | gcmd.respond_info(f"Tool {tool} load") 698 | 699 | def _find_next_available_slot(self, current_slot): 700 | """Find the next available slot with filament for endless spool""" 701 | for i in range(4): 702 | next_slot = (current_slot + 1 + i) % 4 703 | if next_slot != current_slot: 704 | # Check both inventory and ACE status 705 | if (self.inventory[next_slot]["status"] == "ready" and 706 | self._info['slots'][next_slot]['status'] == 'ready'): 707 | return next_slot 708 | return -1 # No available slots 709 | 710 | def _endless_spool_runout_handler(self): 711 | """Handle runout detection for endless spool""" 712 | if not self.endless_spool_enabled or self.endless_spool_in_progress: 713 | return 714 | 715 | current_tool = self.variables.get('ace_current_index', -1) 716 | if current_tool == -1: 717 | return 718 | 719 | try: 720 | sensor_extruder = self.printer.lookup_object("filament_switch_sensor extruder_sensor", None) 721 | if sensor_extruder: 722 | # Check both runout helper and direct endstop state 723 | runout_helper_present = bool(sensor_extruder.runout_helper.filament_present) 724 | endstop_triggered = self._check_endstop_state('extruder_sensor') 725 | 726 | # Log sensor states for debugging (remove after testing) 727 | # logging.info(f"ACE Debug: runout_helper={runout_helper_present}, endstop={endstop_triggered}") 728 | 729 | # Runout detected if filament is not present 730 | if not runout_helper_present or not endstop_triggered: 731 | if not self.endless_spool_runout_detected: # Only trigger once 732 | self.endless_spool_runout_detected = True 733 | self.gcode.respond_info("ACE: Endless spool runout detected, switching immediately") 734 | logging.info(f"ACE: Runout detected - runout_helper={runout_helper_present}, endstop={endstop_triggered}") 735 | # Execute endless spool change immediately 736 | self._execute_endless_spool_change() 737 | except Exception as e: 738 | logging.info(f'ACE: Runout detection error: {str(e)}') 739 | 740 | def _execute_endless_spool_change(self): 741 | """Execute the endless spool toolchange - simplified for extruder sensor only""" 742 | if self.endless_spool_in_progress: 743 | return 744 | 745 | current_tool = self.variables.get('ace_current_index', -1) 746 | next_tool = self._find_next_available_slot(current_tool) 747 | 748 | if next_tool == -1: 749 | self.gcode.respond_info("ACE: No available slots for endless spool, pausing print") 750 | self.gcode.run_script_from_command('PAUSE') 751 | self.endless_spool_runout_detected = False 752 | return 753 | 754 | self.endless_spool_in_progress = True 755 | self.endless_spool_runout_detected = False 756 | 757 | self.gcode.respond_info(f"ACE: Endless spool changing from slot {current_tool} to slot {next_tool}") 758 | 759 | # Mark current slot as empty in inventory 760 | if current_tool >= 0: 761 | self.inventory[current_tool] = {"status": "empty", "color": [0, 0, 0], "material": "", "temp": 0} 762 | # Save updated inventory to persistent variables 763 | self.variables['ace_inventory'] = self.inventory 764 | self.gcode.run_script_from_command(f'SAVE_VARIABLE VARIABLE=ace_inventory VALUE=\'{json.dumps(self.inventory)}\'') 765 | 766 | try: 767 | # Direct endless spool change - no toolchange macros needed for runout response 768 | 769 | # Step 1: Disable feed assist on empty slot 770 | if current_tool != -1: 771 | self._disable_feed_assist(current_tool) 772 | self.wait_ace_ready() 773 | 774 | # Step 2: Feed filament from next slot until it reaches extruder sensor 775 | sensor_extruder = self.printer.lookup_object("filament_switch_sensor extruder_sensor", None) 776 | 777 | # Feed filament from new slot until extruder sensor triggers 778 | self._feed(next_tool, self.toolchange_load_length, self.retract_speed) 779 | self.wait_ace_ready() 780 | 781 | # Wait for filament to reach extruder sensor 782 | while not bool(sensor_extruder.runout_helper.filament_present): 783 | self.dwell(delay=0.1) 784 | 785 | if not bool(sensor_extruder.runout_helper.filament_present): 786 | raise ValueError("Filament stuck during endless spool change") 787 | 788 | # Step 3: Enable feed assist for new slot 789 | self._enable_feed_assist(next_tool) 790 | 791 | # Step 4: Update current index and save state 792 | self.variables['ace_current_index'] = next_tool 793 | self.gcode.run_script_from_command('SAVE_VARIABLE VARIABLE=ace_current_index VALUE=' + str(next_tool)) 794 | 795 | self.endless_spool_in_progress = False 796 | 797 | self.gcode.respond_info(f"ACE: Endless spool completed, now using slot {next_tool}") 798 | 799 | except Exception as e: 800 | self.gcode.respond_info(f"ACE: Endless spool change failed: {str(e)}") 801 | self.gcode.run_script_from_command('PAUSE') 802 | self.endless_spool_in_progress = False 803 | 804 | cmd_ACE_ENABLE_ENDLESS_SPOOL_help = 'Enable endless spool feature' 805 | 806 | cmd_ACE_ENABLE_ENDLESS_SPOOL_help = 'Enable endless spool feature' 807 | 808 | def cmd_ACE_ENABLE_ENDLESS_SPOOL(self, gcmd): 809 | self.endless_spool_enabled = True 810 | 811 | # Save to persistent variables 812 | self.variables['ace_endless_spool_enabled'] = True 813 | self.gcode.run_script_from_command('SAVE_VARIABLE VARIABLE=ace_endless_spool_enabled VALUE=True') 814 | 815 | gcmd.respond_info("ACE: Endless spool enabled (immediate switching on runout, saved to persistent variables)") 816 | 817 | cmd_ACE_DISABLE_ENDLESS_SPOOL_help = 'Disable endless spool feature' 818 | 819 | def cmd_ACE_DISABLE_ENDLESS_SPOOL(self, gcmd): 820 | self.endless_spool_enabled = False 821 | self.endless_spool_runout_detected = False 822 | self.endless_spool_in_progress = False 823 | 824 | # Save to persistent variables 825 | self.variables['ace_endless_spool_enabled'] = False 826 | self.gcode.run_script_from_command('SAVE_VARIABLE VARIABLE=ace_endless_spool_enabled VALUE=False') 827 | 828 | gcmd.respond_info("ACE: Endless spool disabled (saved to persistent variables)") 829 | 830 | cmd_ACE_ENDLESS_SPOOL_STATUS_help = 'Show endless spool status' 831 | 832 | def cmd_ACE_ENDLESS_SPOOL_STATUS(self, gcmd): 833 | status = self.get_status()['endless_spool'] 834 | saved_enabled = self.variables.get('ace_endless_spool_enabled', False) 835 | 836 | gcmd.respond_info(f"ACE: Endless spool status:") 837 | gcmd.respond_info(f" - Currently enabled: {status['enabled']}") 838 | gcmd.respond_info(f" - Saved enabled: {saved_enabled}") 839 | gcmd.respond_info(f" - Mode: Immediate switching on runout detection") 840 | 841 | if status['enabled']: 842 | gcmd.respond_info(f" - Runout detected: {status['runout_detected']}") 843 | gcmd.respond_info(f" - In progress: {status['in_progress']}") 844 | 845 | def find_com_port(self, device_name): 846 | com_ports = serial.tools.list_ports.comports() 847 | for port, desc, hwid in com_ports: 848 | if device_name in desc: 849 | return port 850 | return None 851 | 852 | def cmd_ACE_DEBUG(self, gcmd): 853 | method = gcmd.get('METHOD') 854 | params = gcmd.get('PARAMS', '{}') 855 | 856 | try: 857 | def callback(self, response): 858 | self.gcode.respond_info(str(response)) 859 | 860 | self.send_request(request = {"method": method, "params": json.loads(params)}, callback = callback) 861 | except Exception as e: 862 | self.gcode.respond_info('Error: ' + str(e)) 863 | #self.gcode.respond_info(str(self.find_com_port('ACE'))) 864 | 865 | 866 | def get_status(self, eventtime=None): 867 | status = self._info.copy() 868 | status['endless_spool'] = { 869 | 'enabled': self.endless_spool_enabled, 870 | 'runout_detected': self.endless_spool_runout_detected, 871 | 'in_progress': self.endless_spool_in_progress 872 | } 873 | return status 874 | 875 | def cmd_ACE_SET_SLOT(self, gcmd): 876 | idx = gcmd.get_int('INDEX') 877 | if idx < 0 or idx >= 4: 878 | raise gcmd.error('Invalid slot index') 879 | if gcmd.get_int('EMPTY', 0): 880 | self.inventory[idx] = {"status": "empty", "color": [0, 0, 0], "material": "", "temp": 0} 881 | # Save to persistent variables 882 | self.variables['ace_inventory'] = self.inventory 883 | self.gcode.run_script_from_command(f'SAVE_VARIABLE VARIABLE=ace_inventory VALUE=\'{json.dumps(self.inventory)}\'') 884 | gcmd.respond_info(f"Slot {idx} set to empty") 885 | return 886 | color_str = gcmd.get('COLOR', None) 887 | material = gcmd.get('MATERIAL', "") 888 | temp = gcmd.get_int('TEMP', 0) 889 | if not color_str or not material or temp <= 0: 890 | raise gcmd.error('COLOR, MATERIAL, and TEMP must be set unless EMPTY=1') 891 | color = [int(x) for x in color_str.split(',')] 892 | if len(color) != 3: 893 | raise gcmd.error('COLOR must be R,G,B') 894 | self.inventory[idx] = { 895 | "status": "ready", 896 | "color": color, 897 | "material": material, 898 | "temp": temp 899 | } 900 | # Save to persistent variables 901 | self.variables['ace_inventory'] = self.inventory 902 | self.gcode.run_script_from_command(f'SAVE_VARIABLE VARIABLE=ace_inventory VALUE=\'{json.dumps(self.inventory)}\'') 903 | gcmd.respond_info(f"Slot {idx} set: color={color}, material={material}, temp={temp}") 904 | 905 | def cmd_ACE_QUERY_SLOTS(self, gcmd): 906 | import json 907 | gcmd.respond_info(json.dumps(self.inventory)) 908 | 909 | cmd_ACE_SAVE_INVENTORY_help = 'Manually save current inventory to persistent storage' 910 | 911 | def cmd_ACE_SAVE_INVENTORY(self, gcmd): 912 | self.variables['ace_inventory'] = self.inventory 913 | self.gcode.run_script_from_command(f'SAVE_VARIABLE VARIABLE=ace_inventory VALUE=\'{json.dumps(self.inventory)}\'') 914 | gcmd.respond_info("ACE: Inventory saved to persistent storage") 915 | 916 | cmd_ACE_TEST_RUNOUT_SENSOR_help = 'Test and display runout sensor states' 917 | 918 | def cmd_ACE_TEST_RUNOUT_SENSOR(self, gcmd): 919 | try: 920 | sensor_extruder = self.printer.lookup_object("filament_switch_sensor extruder_sensor", None) 921 | if sensor_extruder: 922 | runout_helper_present = bool(sensor_extruder.runout_helper.filament_present) 923 | endstop_triggered = self._check_endstop_state('extruder_sensor') 924 | 925 | gcmd.respond_info(f"ACE: Extruder sensor states:") 926 | gcmd.respond_info(f" - Runout helper filament present: {runout_helper_present}") 927 | gcmd.respond_info(f" - Endstop triggered: {endstop_triggered}") 928 | gcmd.respond_info(f" - Endless spool enabled: {self.endless_spool_enabled}") 929 | gcmd.respond_info(f" - Current tool: {self.variables.get('ace_current_index', -1)}") 930 | gcmd.respond_info(f" - Runout detected: {self.endless_spool_runout_detected}") 931 | 932 | # Test runout detection logic 933 | would_trigger = not runout_helper_present or not endstop_triggered 934 | gcmd.respond_info(f" - Would trigger runout: {would_trigger}") 935 | else: 936 | gcmd.respond_info("ACE: Extruder sensor not found") 937 | except Exception as e: 938 | gcmd.respond_info(f"ACE: Error testing sensor: {str(e)}") 939 | 940 | cmd_ACE_GET_CURRENT_INDEX_help = 'Get the currently loaded slot index' 941 | 942 | def cmd_ACE_GET_CURRENT_INDEX(self, gcmd): 943 | current_index = self.variables.get('ace_current_index', -1) 944 | gcmd.respond_info(str(current_index)) 945 | 946 | def _on_toolhead_move(self, event): 947 | """Event handler for toolhead move, used for monitoring extruder movement""" 948 | if not self.endless_spool_enabled or self._park_in_progress or self.endless_spool_in_progress: 949 | return 950 | 951 | # Check for runout during any move 952 | self._endless_spool_runout_handler() 953 | 954 | # If runout is detected, track extruder distance 955 | if hasattr(event, 'newpos') and hasattr(event, 'oldpos'): 956 | newpos = event.newpos 957 | oldpos = event.oldpos 958 | if len(newpos) > 3 and len(oldpos) > 3: 959 | e_move = newpos[3] - oldpos[3] 960 | if e_move > 0 and self.endless_spool_runout_detected: 961 | self._endless_spool_check_distance(e_move) 962 | 963 | cmd_ACE_CHANGE_SPOOL_help = 'Change spool for a specific index - INDEX= (retracts filament from tube, unloads if loaded first)' 964 | 965 | def cmd_ACE_CHANGE_SPOOL(self, gcmd): 966 | index = gcmd.get_int('INDEX', None) 967 | 968 | if index is None: 969 | raise gcmd.error('INDEX parameter is required') 970 | 971 | if index < 0 or index >= 4: 972 | raise gcmd.error('Wrong index - must be 0-3') 973 | 974 | gcmd.respond_info(f"ACE: Changing spool for index {index}") 975 | 976 | # Check if this slot is currently loaded (active tool) 977 | current_tool = self.variables.get('ace_current_index', -1) 978 | 979 | if current_tool == index: 980 | # If this is the currently loaded tool, unload it first (T-1) 981 | gcmd.respond_info(f"ACE: Index {index} is currently loaded, unloading first...") 982 | # Create a proper gcode command to unload the tool 983 | unload_cmd = "ACE_CHANGE_TOOL TOOL=-1" 984 | self.gcode.run_script_from_command(unload_cmd) 985 | gcmd.respond_info("ACE: Tool unloaded") 986 | 987 | # Check if slot is not empty (has filament loaded in the system) 988 | slot_status = None 989 | if hasattr(self, '_info') and self._info and 'slots' in self._info: 990 | slot_status = self._info['slots'][index]['status'] 991 | 992 | inventory_status = self.inventory[index]['status'] 993 | 994 | # If slot is not empty or has filament in the system, retract it 995 | if (slot_status and slot_status != 'empty') or (inventory_status and inventory_status != 'empty'): 996 | gcmd.respond_info(f"ACE: Retracting filament from bowden tube for index {index}") 997 | gcmd.respond_info(f"ACE: Retracting {self.bowden_tube_length}mm at {self.retract_speed}mm/min") 998 | 999 | try: 1000 | self._retract(index, self.bowden_tube_length, self.retract_speed) 1001 | gcmd.respond_info(f"ACE: Filament retracted from index {index}") 1002 | except Exception as e: 1003 | gcmd.respond_info(f"ACE: Error during retraction: {str(e)}") 1004 | raise gcmd.error(f"Failed to retract filament: {str(e)}") 1005 | else: 1006 | gcmd.respond_info(f"ACE: Index {index} is already empty, no retraction needed") 1007 | 1008 | gcmd.respond_info(f"ACE: Spool change completed for index {index}") 1009 | 1010 | 1011 | def load_config(config): 1012 | return BunnyAce(config) 1013 | 1014 | 1015 | -------------------------------------------------------------------------------- /KS/acepro.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import gi 4 | 5 | gi.require_version("Gtk", "3.0") 6 | from gi.repository import Gtk, Pango, Gdk, GLib 7 | from ks_includes.screen_panel import ScreenPanel 8 | from ks_includes.widgets.keypad import Keypad 9 | 10 | 11 | class Panel(ScreenPanel): 12 | def __init__(self, screen, title): 13 | super().__init__(screen, title) 14 | self.current_slot_settings = {"type": "PLA", "color": "255,255,255", "temp": "200"} 15 | self.ace_status = {} 16 | self.slot_inventory = [] 17 | self.dryer_enabled = False 18 | self.current_loaded_slot = -1 # Cache the loaded slot 19 | self.numpad_visible = False # Track numpad state 20 | self.endless_spool_enabled = False # Track endless spool status 21 | 22 | # Initialize slot components lists 23 | self.slot_boxes = [] 24 | self.slot_labels = [] 25 | self.slot_color_boxes = [] 26 | self.slot_buttons = [] 27 | 28 | # Store actual slot data for configuration screen 29 | self.slot_data = [ 30 | {"material": "PLA", "color": [255, 255, 255], "temp": 200, "status": "empty"}, 31 | {"material": "PLA", "color": [255, 255, 255], "temp": 200, "status": "empty"}, 32 | {"material": "PLA", "color": [255, 255, 255], "temp": 200, "status": "empty"}, 33 | {"material": "PLA", "color": [255, 255, 255], "temp": 200, "status": "empty"} 34 | ] 35 | 36 | # Create main screen layout 37 | self.create_main_screen() 38 | 39 | # Add custom CSS for rounded boxes and color indicators 40 | self.add_custom_css() 41 | 42 | # Subscribe to saved_variables updates 43 | if hasattr(self._screen.printer, 'klippy') and hasattr(self._screen.printer.klippy, 'subscribe_object'): 44 | try: 45 | self._screen.printer.klippy.subscribe_object("saved_variables", ["variables"]) 46 | logging.info("ACE: Subscribed to saved_variables updates") 47 | except Exception as e: 48 | logging.error(f"ACE: Failed to subscribe to saved_variables: {e}") 49 | 50 | # Initialize loaded slot from saved_variables (will be updated in get_current_loaded_slot) 51 | 52 | # Try to initialize the current loaded slot immediately 53 | self.initialize_loaded_slot() 54 | 55 | def add_custom_css(self): 56 | """Add custom CSS for slot appearance - using specific ACE classes to avoid conflicts""" 57 | css_provider = Gtk.CssProvider() 58 | css = """ 59 | .ace_slot_color_indicator { 60 | border: 1px solid #333333; 61 | border-radius: 3px; 62 | } 63 | 64 | .ace_slot_button { 65 | border-radius: 10px; 66 | background-color: #2a2a2a; 67 | border: 2px solid #444444; 68 | } 69 | 70 | .ace_slot_button:hover { 71 | border-color: #666666; 72 | } 73 | 74 | .ace_slot_loaded { 75 | background-color: white; 76 | color: black; 77 | } 78 | 79 | .ace_slot_loaded .ace_slot_label { 80 | color: black; 81 | } 82 | 83 | .ace_slot_loaded .ace_slot_number { 84 | color: black; 85 | } 86 | 87 | .ace_slot_loaded * { 88 | color: black; 89 | } 90 | 91 | .ace_slot_empty { 92 | background-color: #2a2a2a; 93 | color: white; 94 | } 95 | 96 | .ace_slot_empty .ace_slot_label { 97 | color: white; 98 | } 99 | 100 | .ace_slot_empty .ace_slot_number { 101 | color: white; 102 | opacity: 0.8; 103 | } 104 | 105 | .ace_slot_number { 106 | font-size: 0.9em; 107 | opacity: 0.8; 108 | } 109 | 110 | .ace_slot_label { 111 | color: inherit; 112 | } 113 | 114 | .ace_color_preview { 115 | border: 2px solid #333333; 116 | border-radius: 5px; 117 | min-width: 20px; 118 | min-height: 15px; 119 | } 120 | 121 | .ace_numpad_button { 122 | background-color: #4a4a4a; 123 | color: white; 124 | border: 1px solid #666666; 125 | border-radius: 5px; 126 | font-size: 14px; 127 | font-weight: bold; 128 | } 129 | 130 | .ace_numpad_button:hover { 131 | background-color: #5a5a5a; 132 | } 133 | 134 | .ace_numpad_function { 135 | background-color: #666666; 136 | color: white; 137 | } 138 | """ 139 | css_provider.load_from_data(css.encode()) 140 | 141 | Gtk.StyleContext.add_provider_for_screen( 142 | Gdk.Screen.get_default(), 143 | css_provider, 144 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 145 | ) 146 | 147 | def set_slot_color(self, color_box, rgb_color): 148 | """Set the color of a slot's color indicator""" 149 | r, g, b = rgb_color 150 | color = Gdk.RGBA(r/255.0, g/255.0, b/255.0, 1.0) 151 | color_box.override_background_color(Gtk.StateFlags.NORMAL, color) 152 | 153 | def get_current_loaded_slot(self): 154 | """Get the currently loaded slot""" 155 | try: 156 | # Try to get from printer data first 157 | if hasattr(self._screen, 'printer') and hasattr(self._screen.printer, 'data'): 158 | printer_data = self._screen.printer.data 159 | 160 | # Check for saved_variables 161 | if 'saved_variables' in printer_data: 162 | save_vars = printer_data['saved_variables'] 163 | 164 | if isinstance(save_vars, dict) and 'variables' in save_vars: 165 | variables = save_vars['variables'] 166 | 167 | if 'ace_current_index' in variables: 168 | value = int(variables['ace_current_index']) 169 | self.current_loaded_slot = value 170 | return value 171 | 172 | # Return cached value or default 173 | return getattr(self, 'current_loaded_slot', -1) 174 | 175 | except Exception as e: 176 | logging.error(f"ACE: Error reading ace_current_index: {e}") 177 | return getattr(self, 'current_loaded_slot', -1) 178 | 179 | def initialize_loaded_slot(self): 180 | """Initialize the loaded slot from saved_variables or query ACE status""" 181 | # Try to get the current loaded slot 182 | current_slot = self.get_current_loaded_slot() 183 | 184 | # If we still don't have a valid slot, query ACE for current status 185 | if current_slot == -1: 186 | logging.info("ACE: No loaded slot found, will query ACE status") 187 | # Query ACE for current loaded slot index 188 | if hasattr(self._screen, '_ws') and hasattr(self._screen._ws, 'klippy'): 189 | self._screen._ws.klippy.gcode_script("ACE_GET_CURRENT_INDEX") 190 | logging.info("ACE: Sent ACE_GET_CURRENT_INDEX command") 191 | 192 | def show_number_input(self, title, message, current_value, min_val, max_val, callback): 193 | """Show number input dialog using compact custom numpad""" 194 | # Create a very compact custom numpad that fits in dialog 195 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) 196 | vbox.set_margin_left(5) 197 | vbox.set_margin_right(5) 198 | vbox.set_margin_top(5) 199 | vbox.set_margin_bottom(5) 200 | 201 | # Store callback and constraints 202 | self.temp_input_callback = callback 203 | self.temp_min = min_val 204 | self.temp_max = max_val 205 | 206 | # Compact title 207 | title_label = Gtk.Label(label=f"{message} ({min_val}-{max_val})") 208 | title_label.get_style_context().add_class("description") 209 | vbox.pack_start(title_label, False, False, 0) 210 | 211 | # Entry field with close button 212 | entry_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 213 | 214 | self.temp_entry = Gtk.Entry() 215 | self.temp_entry.set_text(str(current_value)) 216 | self.temp_entry.set_halign(Gtk.Align.CENTER) 217 | self.temp_entry.set_size_request(100, 30) 218 | entry_box.pack_start(self.temp_entry, True, True, 0) 219 | 220 | # Close button 221 | close_btn = self._gtk.Button("cancel", scale=0.6) 222 | close_btn.set_size_request(30, 30) 223 | close_btn.connect("clicked", self.close_temp_dialog) 224 | entry_box.pack_start(close_btn, False, False, 0) 225 | 226 | vbox.pack_start(entry_box, False, False, 2) 227 | 228 | # Compact number grid (smaller buttons) 229 | numpad = Gtk.Grid(row_homogeneous=True, column_homogeneous=True) 230 | numpad.set_row_spacing(2) 231 | numpad.set_column_spacing(2) 232 | 233 | # Number buttons 1-9, 0, backspace, decimal 234 | buttons = [ 235 | ['1', '2', '3'], 236 | ['4', '5', '6'], 237 | ['7', '8', '9'], 238 | ['⌫', '0', '.'] 239 | ] 240 | 241 | for row, button_row in enumerate(buttons): 242 | for col, btn_text in enumerate(button_row): 243 | btn = Gtk.Button(label=btn_text) 244 | btn.set_size_request(50, 35) # Small compact buttons 245 | btn.get_style_context().add_class("numpad_key") 246 | if btn_text == '⌫': 247 | btn.connect("clicked", self.numpad_backspace) 248 | else: 249 | btn.connect("clicked", self.numpad_clicked, btn_text) 250 | numpad.attach(btn, col, row, 1, 1) 251 | 252 | vbox.pack_start(numpad, False, False, 2) 253 | 254 | # OK button 255 | ok_btn = self._gtk.Button("complete", "OK", "color1") 256 | ok_btn.set_size_request(-1, 35) 257 | ok_btn.connect("clicked", self.handle_temp_ok) 258 | vbox.pack_start(ok_btn, False, False, 2) 259 | 260 | # Create dialog with no extra buttons 261 | buttons = [] 262 | 263 | def response_callback(dialog, response_id): 264 | self.temp_input_dialog = None 265 | 266 | self.temp_input_dialog = self._gtk.Dialog(title, buttons, vbox, response_callback) 267 | 268 | def numpad_clicked(self, widget, digit): 269 | """Handle number button clicks""" 270 | current = self.temp_entry.get_text() 271 | self.temp_entry.set_text(current + digit) 272 | 273 | def numpad_backspace(self, widget): 274 | """Handle backspace button""" 275 | current = self.temp_entry.get_text() 276 | if len(current) > 0: 277 | self.temp_entry.set_text(current[:-1]) 278 | 279 | def handle_temp_ok(self, widget): 280 | """Handle OK button click""" 281 | try: 282 | value = int(float(self.temp_entry.get_text())) 283 | if self.temp_min <= value <= self.temp_max: 284 | self.temp_input_callback(value) 285 | self.close_temp_dialog() 286 | else: 287 | self._screen.show_popup_message(f"Value must be between {self.temp_min}-{self.temp_max}") 288 | except (ValueError, TypeError): 289 | self._screen.show_popup_message("Invalid number") 290 | 291 | def close_temp_dialog(self, widget=None): 292 | """Close temperature input dialog""" 293 | if hasattr(self, 'temp_input_dialog') and self.temp_input_dialog: 294 | if hasattr(self._gtk, 'remove_dialog'): 295 | self._gtk.remove_dialog(self.temp_input_dialog) 296 | self.temp_input_dialog = None 297 | 298 | def show_color_picker(self, title, current_color, callback): 299 | """Show very compact color picker dialog""" 300 | # Store the callback for the color picker 301 | self.color_picker_callback = callback 302 | 303 | main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) # Minimal spacing 304 | main_box.set_margin_left(10) # Minimal margins 305 | main_box.set_margin_right(10) 306 | main_box.set_margin_top(10) 307 | main_box.set_margin_bottom(10) 308 | 309 | # Current color values 310 | self.picker_rgb = list(current_color) 311 | 312 | # Single row with preview and RGB display 313 | top_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 314 | 315 | # Color preview 316 | self.color_preview_widget = Gtk.EventBox() 317 | self.color_preview_widget.get_style_context().add_class("ace_color_preview") 318 | self.color_preview_widget.set_size_request(60, 30) # Smaller preview 319 | self.set_color_preview(self.color_preview_widget, self.picker_rgb) 320 | top_row.pack_start(self.color_preview_widget, False, False, 0) 321 | 322 | # RGB values display 323 | self.rgb_label_widget = Gtk.Label(label=f"RGB: {self.picker_rgb[0]},{self.picker_rgb[1]},{self.picker_rgb[2]}") 324 | self.rgb_label_widget.set_halign(Gtk.Align.START) 325 | top_row.pack_start(self.rgb_label_widget, True, True, 0) 326 | 327 | main_box.pack_start(top_row, False, False, 0) 328 | 329 | # Very compact sliders - horizontal layout 330 | sliders_grid = Gtk.Grid() 331 | sliders_grid.set_row_spacing(3) 332 | sliders_grid.set_column_spacing(5) 333 | 334 | # Create RGB sliders with references to update function 335 | self.create_mini_slider("R", 0, 0, sliders_grid) 336 | self.create_mini_slider("G", 1, 1, sliders_grid) 337 | self.create_mini_slider("B", 2, 2, sliders_grid) 338 | 339 | main_box.pack_start(sliders_grid, False, False, 0) 340 | 341 | # Minimal preset colors - single row 342 | #presets_label = Gtk.Label(label="Presets:") 343 | #presets_label.set_halign(Gtk.Align.START) 344 | #main_box.pack_start(presets_label, False, False, 0) 345 | 346 | # Just 6 most common colors in one row 347 | preset_colors = [ 348 | ("W", [255, 255, 255]), 349 | ("K", [0, 0, 0]), 350 | ("R", [255, 0, 0]), 351 | ("G", [0, 255, 0]), 352 | ("B", [0, 0, 255]), 353 | ("Y", [255, 255, 0]) 354 | ] 355 | 356 | preset_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3) 357 | preset_row.set_homogeneous(True) 358 | 359 | for name, rgb in preset_colors: 360 | preset_btn = Gtk.Button() 361 | preset_btn.set_size_request(35, 25) # Very small buttons 362 | preset_btn.set_label(name) 363 | 364 | # Set button colors 365 | preset_color = Gdk.RGBA(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0, 1.0) 366 | preset_btn.override_background_color(Gtk.StateFlags.NORMAL, preset_color) 367 | 368 | # Set text color 369 | brightness = (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) 370 | text_color = Gdk.RGBA(0, 0, 0, 1) if brightness > 128 else Gdk.RGBA(1, 1, 1, 1) 371 | preset_btn.override_color(Gtk.StateFlags.NORMAL, text_color) 372 | 373 | def on_preset_click(widget, preset_rgb): 374 | self.picker_rgb[:] = preset_rgb 375 | self.update_color_preview() 376 | 377 | preset_btn.connect("clicked", on_preset_click, rgb[:]) 378 | preset_row.pack_start(preset_btn, True, True, 0) 379 | 380 | main_box.pack_start(preset_row, False, False, 0) 381 | 382 | buttons = [ 383 | {"name": "Cancel", "response": Gtk.ResponseType.CANCEL}, 384 | {"name": "OK", "response": Gtk.ResponseType.OK} 385 | ] 386 | 387 | # Use the KlipperScreen dialog method 388 | self._gtk.Dialog(title, buttons, main_box, self.color_picker_response) 389 | 390 | def create_mini_slider(self, color_name, color_index, row, grid): 391 | """Create a mini slider for RGB color component""" 392 | # Label 393 | label = Gtk.Label(label=f"{color_name}:") 394 | label.set_size_request(25, -1) 395 | grid.attach(label, 0, row, 1, 1) 396 | 397 | # Value label 398 | value_label = Gtk.Label(label=str(self.picker_rgb[color_index])) 399 | value_label.set_size_request(30, -1) 400 | value_label.set_halign(Gtk.Align.END) 401 | grid.attach(value_label, 1, row, 1, 1) 402 | 403 | # Slider 404 | slider = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 255, 1) 405 | slider.set_value(self.picker_rgb[color_index]) 406 | slider.set_size_request(150, 20) # Compact slider 407 | slider.set_draw_value(False) 408 | 409 | def on_slider_change(widget): 410 | value = int(widget.get_value()) 411 | self.picker_rgb[color_index] = value 412 | value_label.set_text(str(value)) 413 | self.update_color_preview() 414 | 415 | slider.connect("value-changed", on_slider_change) 416 | grid.attach(slider, 2, row, 1, 1) 417 | 418 | def update_color_preview(self): 419 | """Update the color preview in the color picker""" 420 | self.set_color_preview(self.color_preview_widget, self.picker_rgb) 421 | self.rgb_label_widget.set_text(f"RGB: {self.picker_rgb[0]},{self.picker_rgb[1]},{self.picker_rgb[2]}") 422 | 423 | def color_picker_response(self, dialog, response_id): 424 | """Handle color picker dialog response""" 425 | logging.info(f"ACE: Color picker response: {response_id}") 426 | try: 427 | if response_id == Gtk.ResponseType.OK: 428 | logging.info(f"ACE: Color picker OK clicked, RGB: {self.picker_rgb}") 429 | if self.color_picker_callback: 430 | self.color_picker_callback(self.picker_rgb[:]) 431 | else: 432 | logging.info("ACE: Color picker cancelled") 433 | finally: 434 | # Ensure dialog is closed by removing it 435 | if hasattr(self._gtk, 'remove_dialog') and dialog: 436 | self._gtk.remove_dialog(dialog) 437 | logging.info("ACE: Color picker dialog closed") 438 | 439 | def set_color_preview(self, widget, rgb_color): 440 | """Set the color preview widget background""" 441 | r, g, b = rgb_color 442 | color = Gdk.RGBA(r/255.0, g/255.0, b/255.0, 1.0) 443 | widget.override_background_color(Gtk.StateFlags.NORMAL, color) 444 | 445 | def update_slot_loaded_states(self): 446 | """Update all slot loaded states based on ace_current_index""" 447 | current_loaded = self.get_current_loaded_slot() 448 | 449 | logging.info(f"ACE: Current loaded slot: {current_loaded}") 450 | 451 | for slot in range(4): 452 | slot_btn = self.slot_buttons[slot] 453 | if slot == current_loaded: 454 | slot_btn.get_style_context().remove_class("ace_slot_empty") 455 | slot_btn.get_style_context().add_class("ace_slot_loaded") 456 | else: 457 | slot_btn.get_style_context().remove_class("ace_slot_loaded") 458 | slot_btn.get_style_context().add_class("ace_slot_empty") 459 | 460 | # Update status label 461 | if current_loaded != -1: 462 | self.status_label.set_text(f"ACE: Ready - Slot {current_loaded} loaded") 463 | else: 464 | self.status_label.set_text("ACE: Ready") 465 | 466 | def on_endless_spool_toggled(self, switch, state): 467 | """Handle endless spool switch toggle""" 468 | self.endless_spool_enabled = state 469 | 470 | # Send command to ACE system to enable/disable endless spool 471 | if state: 472 | self._screen._ws.klippy.gcode_script("ACE_ENABLE_ENDLESS_SPOOL") 473 | self._screen.show_popup_message("Endless spool enabled", 1) 474 | logging.info("ACE: Endless spool enabled") 475 | else: 476 | self._screen._ws.klippy.gcode_script("ACE_DISABLE_ENDLESS_SPOOL") 477 | self._screen.show_popup_message("Endless spool disabled", 1) 478 | logging.info("ACE: Endless spool disabled") 479 | 480 | def on_slot_clicked(self, widget, slot): 481 | """Handle slot button clicks""" 482 | current_loaded = self.get_current_loaded_slot() 483 | 484 | if current_loaded == slot: 485 | # Clicked on loaded slot - ask to unload 486 | self.show_unload_confirmation(slot) 487 | else: 488 | # Clicked on unloaded slot - ask to load 489 | self.show_load_confirmation(slot) 490 | 491 | def show_load_confirmation(self, slot): 492 | """Show confirmation dialog to load a slot""" 493 | slot_info = self.slot_labels[slot].get_text() 494 | if slot_info == "Empty": 495 | self._screen.show_popup_message("Slot is empty. Configure it first using the settings button.") 496 | return 497 | 498 | current_loaded = self.get_current_loaded_slot() 499 | message = f"Load Slot {slot}?\n\n{slot_info}" 500 | if current_loaded != -1: 501 | message += f"\n\nThis will unload Slot {current_loaded}" 502 | 503 | label = Gtk.Label(label=message) 504 | label.set_line_wrap(True) 505 | label.set_justify(Gtk.Justification.CENTER) 506 | 507 | buttons = [ 508 | {"name": "Cancel", "response": Gtk.ResponseType.CANCEL}, 509 | {"name": "Load", "response": Gtk.ResponseType.OK} 510 | ] 511 | 512 | def load_response(dialog, response_id): 513 | try: 514 | if response_id == Gtk.ResponseType.OK: 515 | # Update cached value immediately for responsive UI 516 | self.current_loaded_slot = slot 517 | self.update_slot_loaded_states() 518 | 519 | # Send the actual command 520 | self._screen._ws.klippy.gcode_script(f"ACE_CHANGE_TOOL TOOL={slot}") 521 | self._screen.show_popup_message(f"Loading slot {slot}...", 1) 522 | 523 | # Return to main screen if we're in configuration mode 524 | if hasattr(self, 'current_config_slot'): 525 | self.return_to_main_screen() 526 | elif response_id == Gtk.ResponseType.CANCEL: 527 | # Just close the dialog - no action needed 528 | logging.info(f"ACE: Load slot {slot} cancelled by user") 529 | finally: 530 | # Ensure dialog is closed 531 | if hasattr(self._gtk, 'remove_dialog') and dialog: 532 | self._gtk.remove_dialog(dialog) 533 | 534 | self._gtk.Dialog(f"Load Slot {slot}", buttons, label, load_response) 535 | 536 | def show_unload_confirmation(self, slot): 537 | """Show confirmation dialog to unload a slot""" 538 | slot_info = self.slot_labels[slot].get_text() 539 | message = f"Unload Slot {slot}?\n\n{slot_info}" 540 | 541 | label = Gtk.Label(label=message) 542 | label.set_line_wrap(True) 543 | label.set_justify(Gtk.Justification.CENTER) 544 | 545 | buttons = [ 546 | {"name": "Cancel", "response": Gtk.ResponseType.CANCEL}, 547 | {"name": "Unload", "response": Gtk.ResponseType.OK} 548 | ] 549 | 550 | def unload_response(dialog, response_id): 551 | try: 552 | if response_id == Gtk.ResponseType.OK: 553 | # Update cached value immediately for responsive UI 554 | self.current_loaded_slot = -1 555 | self.update_slot_loaded_states() 556 | 557 | # Send the actual command 558 | self._screen._ws.klippy.gcode_script(f"ACE_CHANGE_TOOL TOOL=-1") 559 | self._screen.show_popup_message(f"Unloading slot {slot}...", 1) 560 | elif response_id == Gtk.ResponseType.CANCEL: 561 | # Just close the dialog - no action needed 562 | logging.info(f"ACE: Unload slot {slot} cancelled by user") 563 | finally: 564 | # Ensure dialog is closed 565 | if hasattr(self._gtk, 'remove_dialog') and dialog: 566 | self._gtk.remove_dialog(dialog) 567 | 568 | self._gtk.Dialog(f"Unload Slot {slot}", buttons, label, unload_response) 569 | 570 | def activate(self): 571 | """Called when panel is shown""" 572 | logging.info("ACE: Panel activated") 573 | 574 | # Try to initialize the loaded slot from saved_variables again 575 | self.initialize_loaded_slot() 576 | 577 | # Update status which will query ACE and update display 578 | self.update_status() 579 | 580 | def delayed_init(self): 581 | """Delayed initialization to allow save_variables to load""" 582 | logging.info("ACE: Delayed initialization called") 583 | current_slot = self.get_current_loaded_slot() 584 | if current_slot != -1: 585 | logging.info(f"ACE: Delayed init found slot: {current_slot}") 586 | self.update_slot_loaded_states() 587 | return False # Don't repeat the timeout 588 | 589 | def refresh_status(self, widget): 590 | """Manual refresh button""" 591 | # Query ACE data 592 | self._screen._ws.klippy.gcode_script("ACE_QUERY_SLOTS") 593 | 594 | # Query current loaded index 595 | self._screen._ws.klippy.gcode_script("ACE_GET_CURRENT_INDEX") 596 | 597 | # Update loaded state 598 | self.update_slot_loaded_states() 599 | self._screen.show_popup_message("Refreshing spool data...", 1) 600 | 601 | def show_slot_settings(self, widget, slot): 602 | """Show two-column slot configuration screen""" 603 | self.current_config_slot = slot 604 | self.show_slot_config_screen(slot) 605 | 606 | def show_slot_config_screen(self, slot): 607 | """Create compact two-column configuration screen that fits 480px""" 608 | # Load current values from slot data 609 | slot_info = self.slot_data[slot] 610 | self.config_material = slot_info["material"] 611 | self.config_color = slot_info["color"][:] # Copy the color array 612 | self.config_temp = slot_info["temp"] 613 | 614 | logging.info(f"ACE: Loading slot {slot} config - Material: {self.config_material}, Color: {self.config_color}, Temp: {self.config_temp}") 615 | 616 | # Clear current content and create two-column layout 617 | for child in self.content.get_children(): 618 | self.content.remove(child) 619 | 620 | # Create main grid with two columns - compact spacing 621 | main_grid = Gtk.Grid() 622 | main_grid.set_column_homogeneous(True) 623 | main_grid.set_row_spacing(2) # Reduced from 10 624 | main_grid.set_column_spacing(1) # Reduced from 10 625 | main_grid.set_margin_left(5) # Reduced from 15 626 | main_grid.set_margin_right(10) 627 | main_grid.set_margin_top(2) # Reduced from 15 628 | main_grid.set_margin_bottom(1) 629 | 630 | # Left column - Configuration options (compact) 631 | left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) # Reduced from 15 632 | 633 | # Compact title for left column 634 | config_title = Gtk.Label(label=f"Configure Slot {slot}") 635 | config_title.get_style_context().add_class("description") # Smaller than temperature_entry 636 | left_box.pack_start(config_title, False, False, 0) 637 | 638 | # Material selection button - smaller, with current value 639 | self.material_btn = self._gtk.Button("filament", f"Material: {self.config_material}", "color1") 640 | self.material_btn.set_size_request(-1, 45) # Reduced from 60 641 | self.material_btn.connect("clicked", self.show_material_selection) 642 | left_box.pack_start(self.material_btn, False, False, 0) 643 | 644 | # Color selection button with preview - smaller, with current color 645 | color_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) # Reduced from 10 646 | 647 | # Smaller color preview with current color 648 | self.config_color_preview = Gtk.EventBox() 649 | self.config_color_preview.set_size_request(30, 30) # Reduced from 40x40 650 | self.config_color_preview.get_style_context().add_class("ace_color_preview") 651 | self.set_color_preview(self.config_color_preview, self.config_color) 652 | color_box.pack_start(self.config_color_preview, False, False, 0) 653 | 654 | # Smaller color button 655 | self.color_btn = self._gtk.Button("palette", "Select Color", "color2") 656 | self.color_btn.set_size_request(-1, 45) # Reduced from 60 657 | self.color_btn.connect("clicked", self.show_color_selection) 658 | color_box.pack_start(self.color_btn, True, True, 0) 659 | 660 | left_box.pack_start(color_box, False, False, 0) 661 | 662 | # Temperature selection button - smaller, with current value 663 | self.temp_btn = self._gtk.Button("heat-up", f"Temperature: {self.config_temp}°C", "color3") 664 | self.temp_btn.set_size_request(-1, 45) # Reduced from 60 665 | self.temp_btn.connect("clicked", self.show_temperature_selection) 666 | left_box.pack_start(self.temp_btn, False, False, 0) 667 | 668 | # Compact action buttons 669 | action_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) # Reduced from 10 670 | action_box.set_homogeneous(True) 671 | 672 | # Smaller save button 673 | save_btn = self._gtk.Button("complete", "Save", "color1") 674 | save_btn.set_size_request(-1, 40) # Reduced from 50 675 | save_btn.connect("clicked", self.save_slot_config) 676 | action_box.pack_start(save_btn, True, True, 0) 677 | 678 | # Smaller cancel button 679 | cancel_btn = self._gtk.Button("cancel", "Cancel", "color4") 680 | cancel_btn.set_size_request(-1, 40) # Reduced from 50 681 | cancel_btn.connect("clicked", self.cancel_slot_config) 682 | action_box.pack_start(cancel_btn, True, True, 0) 683 | 684 | left_box.pack_end(action_box, False, False, 0) 685 | 686 | # Right column - Selection panels (compact) 687 | self.right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) # Reduced from 10 688 | 689 | # Compact welcome message for right column 690 | welcome_label = Gtk.Label(label="Select an option from the left\nto configure the slot") 691 | welcome_label.set_justify(Gtk.Justification.CENTER) 692 | welcome_label.get_style_context().add_class("description") 693 | self.right_box.pack_start(welcome_label, True, True, 0) 694 | 695 | # Add columns to main grid 696 | main_grid.attach(left_box, 0, 0, 1, 1) 697 | main_grid.attach(self.right_box, 1, 0, 1, 1) 698 | 699 | self.content.add(main_grid) 700 | self.content.show_all() 701 | 702 | def show_material_selection(self, widget): 703 | """Show compact material selection in right column""" 704 | # Clear right column 705 | for child in self.right_box.get_children(): 706 | self.right_box.remove(child) 707 | 708 | # Compact material selection title 709 | title = Gtk.Label(label="Select Material") 710 | title.get_style_context().add_class("description") # Smaller title 711 | self.right_box.pack_start(title, False, False, 0) 712 | 713 | # Compact material list 714 | materials = ["PLA", "ABS", "PETG", "TPU", "ASA", "PVA", "HIPS", "PC"] 715 | 716 | for material in materials: 717 | material_btn = self._gtk.Button("filament", material, "color2") 718 | material_btn.set_size_request(-1, 35) # Reduced from 50 719 | material_btn.connect("clicked", self.select_material, material) 720 | 721 | # Highlight current selection 722 | if material == self.config_material: 723 | material_btn.get_style_context().add_class("button_active") 724 | 725 | self.right_box.pack_start(material_btn, False, False, 3) # Reduced spacing 726 | 727 | self.right_box.show_all() 728 | 729 | def select_material(self, widget, material): 730 | """Handle material selection""" 731 | self.config_material = material 732 | self.material_btn.set_label(f"Material: {material}") 733 | 734 | # Clear right column back to welcome message 735 | self.clear_right_column() 736 | 737 | def show_color_selection(self, widget): 738 | """Show compact color picker in right column""" 739 | # Clear right column 740 | for child in self.right_box.get_children(): 741 | self.right_box.remove(child) 742 | 743 | # Compact color selection title 744 | #title = Gtk.Label(label="Select Color") 745 | #title.get_style_context().add_class("description") 746 | #self.right_box.pack_start(title, False, False, 0) 747 | 748 | # Compact current color preview and RGB display 749 | preview_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) # Reduced spacing 750 | preview_box.set_halign(Gtk.Align.CENTER) 751 | 752 | self.right_color_preview = Gtk.EventBox() 753 | self.right_color_preview.set_size_request(40, 40) # Smaller preview 754 | self.right_color_preview.get_style_context().add_class("ace_color_preview") 755 | self.set_color_preview(self.right_color_preview, self.config_color) 756 | preview_box.pack_start(self.right_color_preview, False, False, 0) 757 | 758 | self.rgb_display = Gtk.Label(label=f"RGB: {self.config_color[0]},{self.config_color[1]},{self.config_color[2]}") 759 | self.rgb_display.get_style_context().add_class("description") 760 | preview_box.pack_start(self.rgb_display, False, False, 0) 761 | 762 | self.right_box.pack_start(preview_box, False, False, 5) # Reduced margin 763 | 764 | # Compact RGB sliders 765 | slider_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) # Reduced spacing 766 | 767 | self.color_sliders = {} 768 | for i, color_name in enumerate(['Red', 'Green', 'Blue']): 769 | color_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) # Reduced spacing 770 | 771 | label = Gtk.Label(label=f"{color_name[0]}:") # Just first letter 772 | label.set_size_request(20, -1) # Smaller label 773 | color_row.pack_start(label, False, False, 0) 774 | 775 | slider = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 255, 1) 776 | slider.set_value(self.config_color[i]) 777 | slider.set_size_request(150, 25) # Smaller slider 778 | slider.set_draw_value(True) 779 | slider.set_value_pos(Gtk.PositionType.RIGHT) 780 | slider.connect("value-changed", self.on_color_slider_changed, i) 781 | self.color_sliders[i] = slider 782 | color_row.pack_start(slider, True, True, 0) 783 | 784 | slider_box.pack_start(color_row, False, False, 0) 785 | 786 | self.right_box.pack_start(slider_box, False, False, 5) 787 | 788 | # Compact color presets 789 | #presets_label = Gtk.Label(label="Presets") 790 | #presets_label.get_style_context().add_class("description") 791 | #self.right_box.pack_start(presets_label, False, False, 3) 792 | 793 | preset_colors = [ 794 | ("White", [255, 255, 255]), 795 | ("Black", [0, 0, 0]), 796 | ("Red", [255, 0, 0]), 797 | ("Green", [0, 255, 0]), 798 | ("Blue", [0, 0, 255]), 799 | ("Yellow", [255, 255, 0]) 800 | ] 801 | 802 | preset_grid = Gtk.Grid() 803 | preset_grid.set_row_spacing(3) # Reduced spacing 804 | preset_grid.set_column_spacing(3) 805 | preset_grid.set_halign(Gtk.Align.CENTER) 806 | 807 | for i, (name, rgb) in enumerate(preset_colors): 808 | preset_btn = Gtk.Button(label=name) 809 | preset_btn.set_size_request(60, 25) # Much smaller buttons 810 | 811 | # Set button color 812 | color = Gdk.RGBA(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0, 1.0) 813 | preset_btn.override_background_color(Gtk.StateFlags.NORMAL, color) 814 | 815 | # Set text color based on brightness 816 | brightness = (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) 817 | text_color = Gdk.RGBA(0, 0, 0, 1) if brightness > 128 else Gdk.RGBA(1, 1, 1, 1) 818 | preset_btn.override_color(Gtk.StateFlags.NORMAL, text_color) 819 | 820 | preset_btn.connect("clicked", self.select_color_preset, rgb[:]) 821 | preset_grid.attach(preset_btn, i % 3, i // 3, 1, 1) # 3 columns instead of 4 822 | 823 | self.right_box.pack_start(preset_grid, False, False, 5) 824 | 825 | # Compact apply color button 826 | apply_btn = self._gtk.Button("complete", "Apply Color", "color1") 827 | apply_btn.set_size_request(-1, 30) # Smaller button 828 | apply_btn.connect("clicked", self.apply_color_selection) 829 | self.right_box.pack_end(apply_btn, False, False, 0) 830 | 831 | self.right_box.show_all() 832 | 833 | def on_color_slider_changed(self, slider, color_index): 834 | """Handle color slider changes""" 835 | value = int(slider.get_value()) 836 | self.config_color[color_index] = value 837 | 838 | # Update preview and RGB display 839 | self.set_color_preview(self.right_color_preview, self.config_color) 840 | self.rgb_display.set_text(f"RGB: {self.config_color[0]},{self.config_color[1]},{self.config_color[2]}") 841 | 842 | def select_color_preset(self, widget, rgb): 843 | """Handle color preset selection""" 844 | self.config_color = rgb[:] 845 | 846 | # Update sliders 847 | for i, value in enumerate(rgb): 848 | self.color_sliders[i].set_value(value) 849 | 850 | # Update preview and RGB display 851 | self.set_color_preview(self.right_color_preview, self.config_color) 852 | self.rgb_display.set_text(f"RGB: {self.config_color[0]},{self.config_color[1]},{self.config_color[2]}") 853 | 854 | def apply_color_selection(self, widget): 855 | """Apply selected color""" 856 | self.set_color_preview(self.config_color_preview, self.config_color) 857 | self.clear_right_column() 858 | 859 | def show_temperature_selection(self, widget): 860 | """Show temperature selection using Keypad like temperature.py""" 861 | # Clear right column 862 | for child in self.right_box.get_children(): 863 | self.right_box.remove(child) 864 | 865 | # Temperature selection title 866 | title = Gtk.Label(label="Set Temperature") 867 | title.get_style_context().add_class("temperature_entry") 868 | self.right_box.pack_start(title, False, False, 0) 869 | 870 | # Create keypad widget exactly like temperature.py 871 | if not hasattr(self, 'config_keypad'): 872 | self.config_keypad = Keypad( 873 | self._screen, 874 | self.handle_temperature_input, 875 | None, # No PID calibrate 876 | self.clear_right_column, # Close callback 877 | ) 878 | 879 | # Set current temperature value 880 | self.config_keypad.clear() 881 | self.config_keypad.labels['entry'].set_text(str(self.config_temp)) 882 | 883 | # Hide PID button 884 | self.config_keypad.show_pid(False) 885 | 886 | # Add keypad to right column 887 | self.right_box.pack_start(self.config_keypad, True, True, 0) 888 | 889 | self.right_box.show_all() 890 | 891 | def handle_temperature_input(self, temp): 892 | """Handle temperature input from keypad""" 893 | try: 894 | temp_value = int(float(temp)) 895 | if 0 <= temp_value <= 300: 896 | self.config_temp = temp_value 897 | self.temp_btn.set_label(f"Temperature: {temp_value}°C") 898 | self.clear_right_column() 899 | else: 900 | self._screen.show_popup_message("Temperature must be between 0-300°C") 901 | except (ValueError, TypeError): 902 | self._screen.show_popup_message("Invalid temperature value") 903 | 904 | def clear_right_column(self, widget=None): 905 | """Clear right column and show welcome message""" 906 | for child in self.right_box.get_children(): 907 | self.right_box.remove(child) 908 | 909 | welcome_label = Gtk.Label(label="Select an option from the left\nto configure the slot") 910 | welcome_label.set_justify(Gtk.Justification.CENTER) 911 | welcome_label.get_style_context().add_class("description") 912 | self.right_box.pack_start(welcome_label, True, True, 0) 913 | 914 | self.right_box.show_all() 915 | 916 | def save_slot_config(self, widget): 917 | """Save slot configuration""" 918 | slot = self.current_config_slot 919 | material = self.config_material 920 | color = f"{self.config_color[0]},{self.config_color[1]},{self.config_color[2]}" 921 | temp = self.config_temp 922 | 923 | # Update the stored slot data 924 | self.slot_data[slot] = { 925 | "material": material, 926 | "color": self.config_color[:], # Copy the color array 927 | "temp": temp, 928 | "status": "ready" 929 | } 930 | 931 | # Update the slot display immediately 932 | self.slot_labels[slot].set_text(f"{material} {temp}°C") 933 | self.set_slot_color(self.slot_color_boxes[slot], self.config_color) 934 | 935 | # Send ACE_SET_SLOT command 936 | cmd = f"ACE_SET_SLOT INDEX={slot} COLOR={color} MATERIAL={material} TEMP={temp}" 937 | self._screen._ws.klippy.gcode_script(cmd) 938 | 939 | self._screen.show_popup_message(f"Slot {slot} configured: {material} {temp}°C", 1) 940 | 941 | # Refresh data and return to main screen 942 | self._screen._ws.klippy.gcode_script("ACE_QUERY_SLOTS") 943 | self.return_to_main_screen() 944 | 945 | def cancel_slot_config(self, widget): 946 | """Cancel configuration and return to main screen""" 947 | self.return_to_main_screen() 948 | 949 | def return_to_main_screen(self): 950 | """Return to main ACE panel screen""" 951 | # Clear content and recreate main screen 952 | for child in self.content.get_children(): 953 | self.content.remove(child) 954 | 955 | # Recreate the main ACE panel layout 956 | self.create_main_screen() 957 | 958 | def create_main_screen(self): 959 | """Create the main ACE panel screen layout""" 960 | # Create main container 961 | main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=15) 962 | main_box.set_margin_left(15) 963 | main_box.set_margin_right(15) 964 | main_box.set_margin_top(15) 965 | main_box.set_margin_bottom(15) 966 | 967 | # Top row with status and endless spool switch 968 | top_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 969 | 970 | # ACE Status Display 971 | self.status_label = Gtk.Label(label="ACE: Ready") 972 | self.status_label.get_style_context().add_class("temperature_entry") 973 | self.status_label.set_size_request(-1, 40) 974 | self.status_label.set_halign(Gtk.Align.START) 975 | top_row.pack_start(self.status_label, True, True, 0) 976 | 977 | # Endless Spool switch section (top right) 978 | endless_spool_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 979 | endless_spool_box.set_halign(Gtk.Align.END) 980 | 981 | # Endless spool label 982 | endless_label = Gtk.Label(label="Endless Spool:") 983 | endless_label.get_style_context().add_class("description") 984 | endless_spool_box.pack_start(endless_label, False, False, 0) 985 | 986 | # Endless spool switch 987 | self.endless_spool_switch = Gtk.Switch() 988 | self.endless_spool_switch.set_active(self.endless_spool_enabled) 989 | self.endless_spool_switch.connect("state-set", self.on_endless_spool_toggled) 990 | endless_spool_box.pack_start(self.endless_spool_switch, False, False, 0) 991 | 992 | top_row.pack_end(endless_spool_box, False, False, 0) 993 | main_box.pack_start(top_row, False, False, 0) 994 | 995 | # Slots container - horizontal layout 996 | slots_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 997 | slots_box.set_homogeneous(True) 998 | 999 | self.slot_boxes = [] 1000 | self.slot_labels = [] 1001 | self.slot_color_boxes = [] 1002 | self.slot_buttons = [] 1003 | 1004 | for slot in range(4): 1005 | # Create clickable slot button (25% taller) 1006 | slot_btn = Gtk.Button() 1007 | slot_btn.get_style_context().add_class("ace_slot_button") # More specific class 1008 | slot_btn.set_relief(Gtk.ReliefStyle.NONE) 1009 | slot_btn.set_size_request(-1, 125) # 25% taller (was ~100px, now 125px) 1010 | slot_btn.connect("clicked", self.on_slot_clicked, slot) 1011 | 1012 | # Slot content box 1013 | slot_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) 1014 | slot_content.set_margin_left(8) 1015 | slot_content.set_margin_right(8) 1016 | slot_content.set_margin_top(10) # Slightly more top margin 1017 | slot_content.set_margin_bottom(10) # Slightly more bottom margin 1018 | 1019 | # Top row: color indicator and status 1020 | top_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 1021 | 1022 | # Color rectangle 1023 | color_box = Gtk.EventBox() 1024 | color_box.set_size_request(20, 20) 1025 | color_box.get_style_context().add_class("ace_slot_color_indicator") # More specific class 1026 | # Default to black color 1027 | self.set_slot_color(color_box, [0, 0, 0]) 1028 | top_row.pack_start(color_box, False, False, 0) 1029 | self.slot_color_boxes.append(color_box) 1030 | 1031 | # Slot label 1032 | slot_label = Gtk.Label(label="Empty") 1033 | slot_label.set_ellipsize(Pango.EllipsizeMode.END) # Fixed: END instead of End 1034 | slot_label.set_halign(Gtk.Align.START) 1035 | slot_label.get_style_context().add_class("ace_slot_label") # More specific class 1036 | top_row.pack_start(slot_label, True, True, 0) 1037 | self.slot_labels.append(slot_label) 1038 | 1039 | slot_content.pack_start(top_row, True, True, 0) 1040 | 1041 | # Slot number label 1042 | slot_num_label = Gtk.Label(label=f"Slot {slot}") 1043 | slot_num_label.get_style_context().add_class("ace_slot_number") # More specific class 1044 | slot_content.pack_start(slot_num_label, False, False, 0) 1045 | 1046 | slot_btn.add(slot_content) 1047 | slots_box.pack_start(slot_btn, True, True, 0) 1048 | self.slot_buttons.append(slot_btn) 1049 | 1050 | main_box.pack_start(slots_box, False, False, 0) 1051 | 1052 | # Settings cogs row - below slot boxes (smaller, closer) 1053 | settings_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 1054 | settings_box.set_homogeneous(True) 1055 | settings_box.set_margin_top(5) # Closer to slot boxes 1056 | 1057 | for slot in range(4): 1058 | # Create container to center the smaller button 1059 | settings_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 1060 | settings_container.set_halign(Gtk.Align.CENTER) 1061 | 1062 | settings_btn = self._gtk.Button("settings", "", "color2") 1063 | settings_btn.set_size_request(36, 27) # 10% smaller (was 40x30, now 36x27) 1064 | settings_btn.connect("clicked", self.show_slot_settings, slot) 1065 | settings_btn.set_tooltip_text(f"Configure Slot {slot}") 1066 | 1067 | settings_container.pack_start(settings_btn, False, False, 0) 1068 | settings_box.pack_start(settings_container, True, True, 0) 1069 | 1070 | main_box.pack_start(settings_box, False, False, 0) 1071 | 1072 | # Bottom row - Refresh and Dryer buttons 1073 | bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=20) 1074 | bottom_box.set_homogeneous(True) 1075 | 1076 | # Refresh button 1077 | refresh_btn = self._gtk.Button("refresh", "Refresh Spool Data", "color3") 1078 | refresh_btn.set_size_request(-1, 50) 1079 | refresh_btn.connect("clicked", self.refresh_status) 1080 | bottom_box.pack_start(refresh_btn, True, True, 0) 1081 | 1082 | # Dryer toggle button 1083 | self.dryer_btn = self._gtk.Button("heat-up", "Start Dryer", "color2") 1084 | self.dryer_btn.set_size_request(-1, 50) 1085 | self.dryer_btn.connect("clicked", self.toggle_dryer_btn) 1086 | bottom_box.pack_start(self.dryer_btn, True, True, 0) 1087 | 1088 | main_box.pack_start(bottom_box, False, False, 0) 1089 | 1090 | self.content.add(main_box) 1091 | self.content.show_all() 1092 | 1093 | # Update status 1094 | self.update_status() 1095 | 1096 | def show_slot_dialog(self, slot): 1097 | """Create ultra-compact dialog for slot settings""" 1098 | # Ultra-minimal dialog box 1099 | dialog_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) # Tiny spacing 1100 | dialog_box.set_margin_left(5) # Minimal margins 1101 | dialog_box.set_margin_right(5) 1102 | dialog_box.set_margin_top(3) 1103 | dialog_box.set_margin_bottom(3) 1104 | 1105 | # Store current values 1106 | self.dialog_material = "PLA" 1107 | self.dialog_color = [255, 255, 255] 1108 | self.dialog_temp = 200 1109 | 1110 | # Material row - ultra compact 1111 | material_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 1112 | material_label = Gtk.Label(label="Mat:") # Shortened label 1113 | material_label.set_size_request(35, -1) # Smaller width 1114 | material_row.pack_start(material_label, False, False, 0) 1115 | 1116 | type_combo = Gtk.ComboBoxText() 1117 | materials = ["PLA", "ABS", "PETG", "TPU", "ASA"] # Removed "Other" 1118 | for material in materials: 1119 | type_combo.append_text(material) 1120 | type_combo.set_active(0) 1121 | type_combo.connect("changed", self.on_material_changed) 1122 | material_row.pack_start(type_combo, True, True, 0) 1123 | dialog_box.pack_start(material_row, False, False, 0) 1124 | 1125 | # Color and Temperature in one row to save space 1126 | color_temp_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) 1127 | 1128 | # Color section 1129 | color_label = Gtk.Label(label="Col:") 1130 | color_label.set_size_request(35, -1) 1131 | color_temp_row.pack_start(color_label, False, False, 0) 1132 | 1133 | # Tiny color preview 1134 | self.dialog_color_preview = Gtk.EventBox() 1135 | self.dialog_color_preview.get_style_context().add_class("ace_color_preview") 1136 | self.dialog_color_preview.set_size_request(20, 15) # Very small 1137 | self.set_color_preview(self.dialog_color_preview, self.dialog_color) 1138 | color_temp_row.pack_start(self.dialog_color_preview, False, False, 0) 1139 | 1140 | # Color picker button - compact 1141 | color_btn = self._gtk.Button("", "Edit", "color1") 1142 | color_btn.set_size_request(50, -1) # Fixed small width 1143 | color_btn.connect("clicked", self.on_color_clicked) 1144 | color_temp_row.pack_start(color_btn, False, False, 0) 1145 | 1146 | # Temperature section in same row 1147 | temp_label = Gtk.Label(label="T:") 1148 | color_temp_row.pack_start(temp_label, False, False, 0) 1149 | 1150 | temp_btn = self._gtk.Button("", f"{self.dialog_temp}°", "color1") # Removed "C" 1151 | temp_btn.set_size_request(50, -1) # Fixed small width 1152 | temp_btn.connect("clicked", self.on_temp_clicked) 1153 | color_temp_row.pack_start(temp_btn, False, False, 0) 1154 | 1155 | dialog_box.pack_start(color_temp_row, False, False, 0) 1156 | 1157 | # Store references for updating 1158 | self.dialog_color_button = color_btn 1159 | self.dialog_temp_button = temp_btn 1160 | 1161 | # Empty slot option - compact 1162 | empty_check = Gtk.CheckButton(label="Mark empty") # Shortened label 1163 | dialog_box.pack_start(empty_check, False, False, 0) 1164 | 1165 | # Store reference for checking 1166 | self.dialog_empty_check = empty_check 1167 | 1168 | buttons = [ 1169 | {"name": "Cancel", "response": Gtk.ResponseType.CANCEL}, 1170 | {"name": "Apply", "response": Gtk.ResponseType.OK} 1171 | ] 1172 | 1173 | def slot_response(dialog, response_id): 1174 | logging.info(f"ACE: Settings dialog response: {response_id}") 1175 | try: 1176 | if response_id == Gtk.ResponseType.OK: 1177 | if self.dialog_empty_check.get_active(): 1178 | # Use ACE_SET_SLOT to mark as empty 1179 | self._screen._ws.klippy.gcode_script(f"ACE_SET_SLOT INDEX={slot} EMPTY=1") 1180 | self._screen.show_popup_message(f"Slot {slot} marked as empty", 1) 1181 | else: 1182 | material = self.dialog_material 1183 | color = f"{self.dialog_color[0]},{self.dialog_color[1]},{self.dialog_color[2]}" 1184 | temp = self.dialog_temp 1185 | 1186 | try: 1187 | # Validate values 1188 | if temp < 0 or temp > 300: 1189 | raise ValueError("Temperature must be between 0-300°C") 1190 | 1191 | # Use ACE_SET_SLOT to update slot data 1192 | cmd = f"ACE_SET_SLOT INDEX={slot} COLOR={color} MATERIAL={material} TEMP={temp}" 1193 | self._screen._ws.klippy.gcode_script(cmd) 1194 | 1195 | self._screen.show_popup_message(f"Slot {slot} configured: {material} {temp}°C", 1) 1196 | 1197 | # Refresh data after setting to get updated info 1198 | self._screen._ws.klippy.gcode_script("ACE_QUERY_SLOTS") 1199 | 1200 | except ValueError as e: 1201 | self._screen.show_popup_message(f"Error: {e}") 1202 | else: 1203 | logging.info("ACE: Settings dialog cancelled") 1204 | except Exception as e: 1205 | logging.error(f"ACE: Error in slot_response: {e}") 1206 | finally: 1207 | # Ensure dialog cleanup 1208 | if hasattr(self._gtk, 'remove_dialog') and dialog: 1209 | self._gtk.remove_dialog(dialog) 1210 | logging.info("ACE: Settings dialog closed") 1211 | 1212 | self._gtk.Dialog(f"Slot {slot} Settings", buttons, dialog_box, slot_response) 1213 | 1214 | def on_material_changed(self, combo): 1215 | """Handle material combo change""" 1216 | self.dialog_material = combo.get_active_text() 1217 | 1218 | def on_color_clicked(self, widget): 1219 | """Handle color picker button click""" 1220 | def color_callback(rgb_values): 1221 | logging.info(f"ACE: Color callback received: {rgb_values}") 1222 | self.dialog_color = rgb_values 1223 | self.dialog_color_button.set_label("Edit") # Keep consistent label 1224 | self.set_color_preview(self.dialog_color_preview, rgb_values) 1225 | 1226 | self.show_color_picker("Choose Color", self.dialog_color, color_callback) 1227 | 1228 | def on_temp_clicked(self, widget): 1229 | """Handle temperature button click""" 1230 | def temp_callback(value): 1231 | self.dialog_temp = value 1232 | self.dialog_temp_button.set_label(f"{value}°") 1233 | 1234 | # Use a simple number entry approach that works within dialogs 1235 | self.show_number_input("Set Temperature", "Enter temperature (0-300°C):", 1236 | self.dialog_temp, 0, 300, temp_callback) 1237 | 1238 | def toggle_dryer_btn(self, widget): 1239 | """Toggle dryer on/off""" 1240 | if self.dryer_enabled: 1241 | # Stop dryer 1242 | self._screen._ws.klippy.gcode_script("ACE_STOP_DRYING") 1243 | self.dryer_btn.set_label("Start Dryer") 1244 | self.dryer_btn.get_style_context().remove_class("color4") 1245 | self.dryer_btn.get_style_context().add_class("color2") 1246 | self.dryer_enabled = False 1247 | self._screen.show_popup_message("Dryer stopped", 1) 1248 | else: 1249 | # Start dryer - show temperature dialog 1250 | self.show_dryer_dialog() 1251 | 1252 | def show_dryer_dialog(self): 1253 | """Show dialog to set dryer temperature""" 1254 | def dryer_callback(value): 1255 | # Start dryer 1256 | self._screen._ws.klippy.gcode_script(f"ACE_START_DRYING TEMP={value} DURATION=240") 1257 | self.dryer_btn.set_label("Stop Dryer") 1258 | self.dryer_btn.get_style_context().remove_class("color2") 1259 | self.dryer_btn.get_style_context().add_class("color4") 1260 | self.dryer_enabled = True 1261 | self._screen.show_popup_message(f"Dryer started at {value}°C", 1) 1262 | 1263 | self.show_number_input("Start Dryer", "Enter dryer temperature:", 45, 35, 55, dryer_callback) 1264 | 1265 | def update_status(self): 1266 | """Update ACE status and slot information""" 1267 | # Query ACE data 1268 | self._screen._ws.klippy.gcode_script("ACE_QUERY_SLOTS") 1269 | 1270 | # Query current loaded index 1271 | self._screen._ws.klippy.gcode_script("ACE_GET_CURRENT_INDEX") 1272 | 1273 | # Query endless spool status 1274 | self._screen._ws.klippy.gcode_script("ACE_ENDLESS_SPOOL_STATUS") 1275 | 1276 | # Update loaded states 1277 | self.update_slot_loaded_states() 1278 | 1279 | def process_update(self, action, data): 1280 | """Process updates from Klipper""" 1281 | if action == "notify_status_update": 1282 | # Check for saved_variables updates 1283 | if "saved_variables" in data: 1284 | save_vars = data["saved_variables"] 1285 | 1286 | if isinstance(save_vars, dict) and "variables" in save_vars: 1287 | variables = save_vars["variables"] 1288 | if "ace_current_index" in variables: 1289 | new_value = int(variables["ace_current_index"]) 1290 | logging.info(f"ACE: ace_current_index updated to: {new_value}") 1291 | if new_value != self.current_loaded_slot: 1292 | self.current_loaded_slot = new_value 1293 | self.update_slot_loaded_states() 1294 | 1295 | if action == "notify_gcode_response": 1296 | # Parse different types of ACE responses 1297 | response_str = str(data).strip() 1298 | logging.info(f"ACE: Received gcode response: {response_str}") 1299 | 1300 | # Look for ACE_QUERY_SLOTS response - starts with "// [" 1301 | if response_str.startswith("// [") and response_str.endswith("]"): 1302 | try: 1303 | # Remove the "// " prefix and parse JSON 1304 | json_str = response_str[3:].strip() # Remove "// " prefix 1305 | slot_data = json.loads(json_str) 1306 | if isinstance(slot_data, list) and len(slot_data) > 0: 1307 | logging.info(f"ACE: Parsed slot data from ACE_QUERY_SLOTS: {slot_data}") 1308 | self.update_slots_from_data(slot_data) 1309 | except json.JSONDecodeError as e: 1310 | logging.error(f"ACE: JSON decode error: {e}") 1311 | 1312 | # Look for ACE_GET_CURRENT_INDEX response - simple format like "// 0" or "// -1" 1313 | elif response_str.startswith("// ") and response_str[3:].strip().lstrip('-').isdigit(): 1314 | try: 1315 | # Extract index number from response like "// 0", "// 2", or "// -1" 1316 | current_index = int(response_str[3:].strip()) 1317 | logging.info(f"ACE: Got current index from ACE_GET_CURRENT_INDEX: {current_index}") 1318 | if current_index != self.current_loaded_slot: 1319 | self.current_loaded_slot = current_index 1320 | self.update_slot_loaded_states() 1321 | except (ValueError, IndexError) as e: 1322 | logging.error(f"ACE: Error parsing ACE_GET_CURRENT_INDEX response '{response_str}': {e}") 1323 | 1324 | # Look for endless spool status responses - check for "Currently enabled" line with // prefix 1325 | elif response_str.startswith("// - Currently enabled:"): 1326 | if "Currently enabled: True" in response_str: 1327 | self.endless_spool_enabled = True 1328 | self.endless_spool_switch.set_active(True) 1329 | logging.info("ACE: Endless spool currently enabled") 1330 | elif "Currently enabled: False" in response_str: 1331 | self.endless_spool_enabled = False 1332 | self.endless_spool_switch.set_active(False) 1333 | logging.info("ACE: Endless spool currently disabled") 1334 | 1335 | # Look for ACE command responses that might indicate tool changes 1336 | elif "ACE:" in response_str: 1337 | logging.info(f"ACE: Command response: {response_str}") 1338 | 1339 | # Look for tool change confirmations 1340 | if "tool" in response_str.lower() and any(word in response_str.lower() for word in ["loaded", "changed", "active"]): 1341 | try: 1342 | # Try to extract slot number from response 1343 | import re 1344 | match = re.search(r'(\d+)', response_str) 1345 | if match: 1346 | new_slot = int(match.group(1)) 1347 | if 0 <= new_slot <= 3: 1348 | logging.info(f"ACE: Tool change detected, updating to slot {new_slot}") 1349 | self.current_loaded_slot = new_slot 1350 | self.update_slot_loaded_states() 1351 | except Exception as e: 1352 | logging.error(f"ACE: Error parsing tool change response: {e}") 1353 | 1354 | def update_slots_from_data(self, slot_data): 1355 | """Update slot display from ACE_QUERY_SLOTS data""" 1356 | logging.info(f"ACE: Updating slots from ACE_QUERY_SLOTS data: {slot_data}") 1357 | 1358 | for i, slot in enumerate(slot_data): 1359 | if i < 4: # Ensure we don't exceed our 4 slots 1360 | if slot.get('status') == 'ready': 1361 | material = slot.get('material', 'PLA') 1362 | temp = slot.get('temp', 200) 1363 | color = slot.get('color', [255, 255, 255]) 1364 | 1365 | # Store the actual slot data 1366 | self.slot_data[i] = { 1367 | "material": material, 1368 | "color": color[:], # Copy the color array 1369 | "temp": temp, 1370 | "status": "ready" 1371 | } 1372 | 1373 | self.slot_labels[i].set_text(f"{material} {temp}°C") 1374 | self.set_slot_color(self.slot_color_boxes[i], color) 1375 | logging.info(f"ACE: Updated slot {i}: {material} {temp}°C, color: {color}") 1376 | else: 1377 | # Store empty slot data 1378 | self.slot_data[i] = { 1379 | "material": "PLA", 1380 | "color": [255, 255, 255], 1381 | "temp": 200, 1382 | "status": "empty" 1383 | } 1384 | 1385 | self.slot_labels[i].set_text("Empty") 1386 | self.set_slot_color(self.slot_color_boxes[i], [0, 0, 0]) 1387 | logging.info(f"ACE: Slot {i} marked as empty") 1388 | 1389 | # Update loaded states after updating slot data 1390 | self.update_slot_loaded_states() --------------------------------------------------------------------------------