[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to identify issues\ntitle: \"[BUG]\"\nlabels: bug\nassignees: Anonoei\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Printer (please complete the following information):**\n - Printer: [e.g. Voron 2.4]\n - Kinematics [e.g. CoreXY]\n - [ ] klippy.log attached\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[ENHANCE]\"\nlabels: enhancement\nassignees: Anonoei\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Anonoei\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Klipper Auto Speed\r\n Klipper module for automatically calculating your printer's maximum acceleration/velocity\r\n\r\n*With one copy/paste and one line in your configuration, automatically optimize your printer's motion*\r\n\r\nThis module automatically performs movements on the *x*, *y*, *x-diagonal*, *y-diagonal*, and *z* axes, and measures your steppers missed steps at various accelerations/velocities.\r\nWith the default configuration, this may take *awhile* (~10 minutes).\r\nMost of the testing time is waiting for your printer to home.\r\nOn my printer with default settings (except MAX_MISSED), it takes ~3.5 minutes for acceleration, and ~5 minutes for velocity.\r\n\r\n**Sensorless homing**: If you're using sensorless homing `MAX_MISSED=1.0` is probably too low.\r\nThe endstop variance check will tell you how many steps you lose when homing.\r\nFor instance, on my printer I lose around 0-4.2 steps each home.\r\nI run `AUTO_SPEED MAX_MISSED=10.0` to account for that variance, and occasional wildly different endstop results.\r\n\r\n**This module is under development**, and has only been validated on CoreXY printers: You may run into issues or bugs, feel free to use the discord channel, or post an issue here.\r\n - [Discord - DOOMCUBE User Projects](https://discord.com/channels/825469421346226226/1162192150822404106)\r\n\r\nYour printer shouldn't have any crashes due to the movement patterns used, and re-homing before/after each test, so it's safe to walk away and let it do it's thing.\r\n\r\nUsing Ellis' pattern (AUTO_SPEED_VALIDATE) is **NOT** a safe movement pattern. Please ensure your toolhead isn't crashing before walking away.\r\n\r\n# Table of Contents\r\n - [Overview](https://github.com/Anonoei/klipper_auto_speed#overview)\r\n - [Example Usage](https://github.com/Anonoei/klipper_auto_speed#example-usage)\r\n - [Roadmap](https://github.com/Anonoei/klipper_auto_speed#roadmap)\r\n - [How does it work](https://github.com/Anonoei/klipper_auto_speed#how-does-it-work)\r\n - [Using Klipper Auto Speed](https://github.com/Anonoei/klipper_auto_speed#using-klipper-auto-speed)\r\n   - [Installation](https://github.com/Anonoei/klipper_auto_speed#installation)\r\n     - [Moonraker Update Manager](https://github.com/Anonoei/klipper_auto_speed#moonraker-update-manager)\r\n   - [Configuration](https://github.com/Anonoei/klipper_auto_speed#configuration)\r\n   - [Macros](https://github.com/Anonoei/klipper_auto_speed#macro)\r\n     - [AUTO_SPEED](https://github.com/Anonoei/klipper_auto_speed#auto_speed)\r\n     - [AUTO_SPEED_ACCEL](https://github.com/Anonoei/klipper_auto_speed#auto_speed_accel)\r\n     - [AUTO_SPEED_VELOCITY](https://github.com/Anonoei/klipper_auto_speed#auto_speed_velocity)\r\n     - [AUTO_SPEED_VALIDATE](https://github.com/Anonoei/klipper_auto_speed#auto_speed_validate)\r\n     - [AUTO_SPEED_GRAPH](https://github.com/Anonoei/klipper_auto_speed#auto_speed_graph)\r\n - [Console Output](https://github.com/Anonoei/klipper_auto_speed#console-output)\r\n\r\n## Overview\r\n - License: MIT\r\n\r\n## Example Usage\r\n- Default usage (find max accel/velocity)\r\n  - `AUTO_SPEED`\r\n- Find maximum acceleration on y axis\r\n  - `AUTO_SPEED_ACCEL AXIS=\"y\"`\r\n- Find maximum acceleration on y, then x axis\r\n  - `AUTO_SPEED_VELOCITY AXIS=\"y,x\"`\r\n- Validate your printer's current accel/velocity (Ellis' test pattern)\r\n  - `AUTO_SPEED_VALIDATE`\r\n- Graph your printer's max velocity/accel\r\n  - `AUTO_SPEED_GRAPH`\r\n- Graph your printer's max velocity/accel between v100 and v1000, over 9 steps\r\n  - `AUTO_SPEED_GRAPH VELOCITY_MIN=100 VELOCITY_MAX=1000 VELOCITY_DIV=9`\r\n \r\n## Roadmap\r\n - [ ] Export printer results as a 'benchmark' to a database to see average speeds for different printers\r\n - [ ] Make _ACCEL/_VELOCITY smarter, based on printer size\r\n - [ ] Add support for running through moonraker (enables scripting different commands, arguments)\r\n - [ ] Save validated/measured results to printer config (like SAVE_CONFIG)\r\n - [ ] Couple ACCEL/VELOCITY similar to AUTO_SPEED_GRAPH\r\n   - [ ] Add AUTO_SPEED ACCEL=10000 - to find what velocity lets you use accel 10000\r\n   - [ ] Add AUTO_SPEED VELOC=500 - to find what accel lets you use velocity 500\r\n   - [ ] Make AUTO_SPEED measure different accels/velocity to find the best values based on printer size\r\n - [ ] Variable motor current\r\n - [ ] Variable homing speed\r\n - [X] Add testing Z axis\r\n - [X] Reduce code duplication\r\n - [X] Check kinematics to find best movement patterns\r\n - [X] Update calculated accel/velocity depending on test to be more accurate\r\n - [X] Update axis movement logic\r\n\r\n## How does it work?\r\n 1. Home your printer\r\n 2. If your print is enclosed, heat soak it. You want to run this module in the typical state your printer is in when you're printing.\r\n 3. Run `AUTO_SPEED`\r\n    1. Prepare\r\n       1. Make sure the printer is level\r\n       2. Check endstop variance\r\n          - Validate the endstops are accurate enough for `MAX_MISSED`\r\n    2. Find the maximum acceleration\r\n       - Perform a binary search between `ACCEL_MIN` and `ACCEL_MAX`\r\n       1. Home, and save stepper start steps\r\n       2. Perform the movement check on the specified axis\r\n       3. Home, and save stepper stop steps\r\n       4. If difference between start/stop steps is more than `max_missed`, go to next step\r\n    3. Find maximum velocity\r\n       - Perform a binary search between `VELOCITY_MIN` and `VELOCITY_MAX`\r\n       1. Home, and save stepper start steps\r\n       2. Perform the movement check on the specified axis\r\n       3. Home, and save stepper stop steps\r\n       4. If difference between start/stop steps is more than `max_missed`, go to next step\r\n    4. Show results\r\n\r\n## Using Klipper Auto Speed\r\n\r\n### Moonraker Update Manager\r\n```\r\n[update_manager klipper_auto_speed]\r\ntype: git_repo\r\npath: ~/klipper_auto_speed\r\norigin: https://github.com/anonoei/klipper_auto_speed.git\r\nprimary_branch: main\r\ninstall_script: install.sh\r\nmanaged_services: klipper\r\n```\r\n\r\n### Installation\r\n To install this module you need to clone the repository and run the `install.sh` script.\r\n **Depending on when you installed klipper, you may also need to [update your klippy-env python version.](https://github.com/Anonoei/klipper_auto_speed#update-klippy-env)**\r\n\r\n#### Automatic installation\r\n```\r\ncd ~\r\ngit clone https://github.com/Anonoei/klipper_auto_speed.git\r\ncd klipper_auto_speed\r\n./install.sh\r\n```\r\n\r\n#### Manual installation\r\n1.  Clone the repository\r\n    1. `cd ~`\r\n    2. `git clone https://github.com/Anonoei/klipper_auto_speed.git`\r\n    3. `cd klipper_auto_speed`\r\n2.  Link auto_speed to klipper\r\n    1. `ln -sf ~/klipper_auto_speed/auto_speed.py ~/klipper/klippy/extras/auto_speed.py`\r\n3.  Install matplotlib\r\n    1.  `~/klippy-env/bin/python -m pip install matplotlib`\r\n4.  Restart klipper\r\n    1. `sudo systemctl restart klipper`\r\n\r\n#### Update klippy-env\r\n 1. `sudo apt install python3`\r\n 2. `sudo apt install python3-numpy`\r\n 3. `sudo systemctl stop klipper`\r\n 4. `python3 -m venv --update ~/klippy-env`\r\n 5. `~/klippy-env/bin/pip install -r \"~/klipper/scripts/klippy-requirements.txt\"`\r\n\r\n### Configuration\r\nPlace this in your printer.cfg\r\n```\r\n[auto_speed]\r\n```\r\nThe values listed below are the defaults Auto Speed uses. You can include them if you wish to change their values or run into issues.\r\n```\r\n[auto_speed]\r\n#axis: diag_x, diag_y  ; One or multiple of `x`, `y`, `diag_x`, `diag_y`, `z`\r\n\r\n#margin: 20            ; How far away from your axes to perform movements\r\n\r\n#settling_home: 1      ; Perform settling home before starting Auto Speed\r\n#max_missed: 1.0       ; Maximum full steps that can be missed\r\n#endstop_samples: 3    ; How many endstop samples to take for endstop variance\r\n\r\n#accel_min: 1000.0     ; Minimum acceleration test may try\r\n#accel_max: 50000.0    ; Maximum acceleration test may try\r\n#accel_accu: 0.05      ; Keep binary searching until the result is within this percentage\r\n\r\n#velocity_min: 50.0    ; Minimum velocity test may try\r\n#velocity_max: 5000.0  ; Maximum velocity test may try\r\n#velocity_accu: 0.05   ; Keep binary searching until the result is within this percentage\r\n\r\n#derate: 0.8           ; Derate discovered results by this amount\r\n\r\n#validate_margin: Unset      ; Margin for VALIDATE, Defaults to margin\r\n#validate_inner_margin: 20.0 ; Margin for VALIDATE inner pattern\r\n#validate_iterations: 50     ; Perform VALIDATE pattern this many times\r\n\r\n#results_dir: ~/printer_data/config ; Destination directory for graphs\r\n```\r\n\r\n### Macro\r\nAuto Speed is split into 5 separate macros. The default `AUTO_SPEED` automatically calls the other three (`AUTO_SPEED_ACCEL`, `AUTO_SPEED_VELOCITY`, `AUTO_SPEED_VALIDATE`). You can use any argument from those macros when you call `AUTO_SPEED`.\r\n\r\nYou can also use `AUTO_SPEED_GRAPH` to find your printers velocity-to-accel relationship.\r\n\r\n#### AUTO_SPEED\r\n `AUTO_SPEED` finds maximum acceleration, velocity, and validates results at the end.\r\nArgument          | Default | Description\r\n----------------- | ------- | -----------\r\nAXIS              | Unset   | Perform test on these axes, defaults to diag_x, diag_y\r\nZ                 | 50      | Z position to run Auto Speed\r\nMARGIN            | 20      | How far away from your axis maximums to perform the test movement\r\nSETTLING_HOME     | 1       | Perform settling home before starting Auto Speed\r\nMAX_MISSED        | 1.0     | Maximum full steps that can be missed\r\nENDSTOP_SAMPLES   | 3       | How many endstop samples to take for endstop variance\r\nTEST_ATTEMPTS     | 2       | Re-test this many times if test fails\r\nACCEL_MIN         | 1000.0  | Minimum acceleration test may try\r\nACCEL_MAX         | 50000.0 | Maximum acceleration test may try\r\nACCEL_ACCU        | 0.05    | Keep binary searching until the result is within this percentage\r\nVELOCITY_MIN      | 50.0    | Minimum velocity test may try\r\nVELOCITY_MAX      | 5000.0  | Maximum velocity test may try\r\nVELOCITY_ACCU     | 0.05    | Keep binary searching until the result is within this percentage\r\nLEVEL             | 1       | Level the printer if it's not leveled\r\nVARIANCE          | 1       | Check endstop variance\r\n\r\n#### AUTO_SPEED_ACCEL\r\n `AUTO_SPEED_ACCEL` find maximum acceleration\r\n Argument   | Default | Description\r\n ---------- | ------- | -----------\r\n AXIS       | Unset   | Perform test on these axes, defaults to diag_x, diag_y\r\n MARGIN     | 20.0    | Used when DIST is 0.0, how far away from axis to perform movements\r\n DERATE     | 0.8     | How much to derate maximum values for the recommended max\r\n MAX_MISSED | 1.0     | Maximum fulls steps that can be missed\r\n ACCEL_MIN  | 1000.0  | Minimum acceleration test may try\r\n ACCEL_MAX  | 50000.0 | Maximum acceleration test may try\r\n ACCEL_ACCU | 0.05    | Keep binary searching until the result is within this percentage\r\n\r\n#### AUTO_SPEED_VELOCITY\r\n `AUTO_SPEED_VELOCITY` finds maximum velocity\r\n Argument      | Default | Description\r\n ------------- | ------- | -----------\r\n AXIS          | Unset   | Perform test on these axes, defaults to diag_x, diag_y\r\n MARGIN        | 20.0    | Used when DIST is 0.0, how far away from axis to perform movements\r\n DERATE        | 0.8     | How much to derate maximum values for the recommended max\r\n MAX_MISSED    | 1.0     | Maximum fulls steps that can be missed\r\n VELOCITY_MIN  | 100.0   | Minimum velocity test may try\r\n VELOCITY_MAX  | 5000.0  | Maximum velocity test may try\r\n VELOCITY_ACCU | 0.05    | Keep binary searching until the result is within this percentage\r\n\r\n#### AUTO_SPEED_VALIDATE\r\n `AUTO_SPEED_VALIDATE` validates a specified acceleration/velocity, using [Ellis' TEST_SPEED Pattern](https://github.com/AndrewEllis93/Print-Tuning-Guide/blob/main/macros/TEST_SPEED.cfg)\r\n Argument              | Default | Description\r\n --------------------- | ------- | -----------\r\n MAX_MISSED            | 1.0     | Maximum fulls steps that can be missed\r\n VALIDATE_MARGIN       | 20.0    | Margin axes max/min pattern can move to\r\n VALIDATE_INNER_MARGIN | 20.0    | Margin from axes center pattern can move to\r\n VALIDATE_ITERATIONS   | 50      | Repeat the pattern this many times\r\n ACCEL                 | Unset   | Defaults to current max accel\r\n VELOCITY              | Unset   | Defaults to current max velocity\r\n\r\n\r\n#### AUTO_SPEED_GRAPH\r\n `AUTO_SPEED_GRAPH` graphs your printer's velocity-to-accel relationship on specified axes\r\n You must specify `VELOCITY_MIN` and `VELOCITY_MAX`.\r\n\r\n Argument        | Default | Description\r\n --------------- | ------- | -----------\r\n AXIS            | Unset   | Perform test on these axes, defaults to diag_x, diag_y\r\n MARGIN          | 20.0    | Used when DIST is 0.0, how far away from axis to perform movements\r\n DERATE          | 0.8     | How much to derate maximum values for the recommended max\r\n MAX_MISSED      | 1.0     | Maximum fulls steps that can be missed\r\n VELOCITY_MIN    | Unset   | Minimum velocity test may try\r\n VELOCITY_MAX    | Unset   | Maximum velocity test may try\r\n VELOCITY_DIV    | 5       | How many velocities to test\r\n VELOCITY_ACCU   | 0.05    | Keep binary searching until the result within this percent\r\n ACCEL_MIN_SLOPE | 100     | Calculated min slope value $\\frac{10000}{velocity \\div slope}$\r\n ACCEL_MAX_SLOPE | 1800    | Calculated max slope value $\\frac{10000}{velocity \\div slope}$\r\n\r\n## Console Output\r\n Console output is slightly different depending on whether testing acceleration/velocity, and which axis is being tested.\r\n\r\n - `axis` is one of `x`, `y`, `diag_x`, `diag_y`, `z`\r\n - The three times after `after` are (first home time)/(movement time)/(end home time)\r\n - `#`s before decimals are variable, `#`s after decimals are static\r\n\r\n### Acceleration tests\r\n```\r\nAUTO SPEED accel on `axis` try # (#.##s)\r\nMoved #.##mm at a###/v### after #.##/#.##/#.##s\r\nMissed X #.##, Y #.##\r\n```\r\nExample:\r\n```\r\nAUTO SPEED accel on diag_x try 1 (19.66s)\r\nMoved 1.43mm at a17333/v241 after 8.92/0.30/9.93s\r\nMissed X 0.31, Y 2.00\r\n```\r\n\r\n### Velocity tests\r\n```\r\nAUTO SPEED velocity on `axis` try # (#.##s)\r\nMoved #.##mm at a###/v### after #.##/#.##/#.##s\r\nMissed X #.##, Y #.##\r\n```\r\nExample:\r\n```\r\nAUTO SPEED velocity on diag_y try 1 (23.91s)\r\nMoved 13.44mm at a91456/v1700 after 8.92/0.31/13.87s\r\nMissed X 0.06, Y 132.00\r\n```\r\n\r\n### Acceleration results\r\n```\r\nAUTO SPEED found maximum acceleration after #.##s\r\n| `AXIS 1` max: ###\r\n| `AXIS 2` max: ###\r\n\r\nRecommended values:\r\n| `AXIS 1` max: ###\r\n| `AXIS 2` max: ###\r\nRecommended acceleration: ###\r\n```\r\nExample:\r\n```\r\nAUTO SPEED found maximum acceleration after 218.00s\r\n| DIAG X max: 48979\r\n| DIAG Y max: 48979\r\n\r\nRecommended values:\r\n| DIAG X max: 39183\r\n| DIAG Y max: 39183\r\nRecommended acceleration: 39183\r\n```\r\n\r\n### Velocity results\r\n```\r\nAUTO SPEED found maximum velocity after #.##s\r\n| `AXIS 1` max: ###\r\n| `AXIS 2` max: ###\r\n\r\nRecommended values\r\n| `AXIS 1` max: ###\r\n| `AXIS 2` max: ###\r\nRecommended velocity: ###\r\n```\r\nExample:\r\n```\r\nAUTO SPEED found maximum velocity after 307.60s\r\n| DIAG X max: 577\r\n| DIAG Y max: 552\r\n\r\nRecommended values\r\n| DIAG X max: 462\r\n| DIAG Y max: 442\r\nRecommended velocity: 442\r\n```\r\n\r\n### Recommended results\r\n```\r\nAUTO SPEED found recommended acceleration and velocity after #.##s\r\n| `AXIS 1` max: a### v###\r\n| `AXIS 2`: a### v###\r\nRecommended accel: ###\r\nRecommended velocity: ###\r\n```\r\nExample:\r\n```\r\nAUTO SPEED found recommended acceleration and velocity after 525.61s\r\n| DIAG X max: a39183 v462\r\n| DIAG Y max: a39183 v442\r\nRecommended accel: 39183\r\nRecommended velocity: 442\r\n```\r\n"
  },
  {
    "path": "auto_speed.py",
    "content": "# Find your printers max speed before losing steps\r\n#\r\n# Copyright (C) 2024 Anonoei <dev@anonoei.com>\r\n#\r\n# This file may be distributed under the terms of the MIT license.\r\n\r\ndef load_config(config): # Called by klipper from [auto_speed]\r\n    try:\r\n        from .autospeed import AutoSpeed\r\n    except ImportError:\r\n        raise ImportError(f\"Please re-run klipper_auto_speed/install.sh\")\r\n    from .autospeed import AutoSpeed\r\n    return AutoSpeed(config)"
  },
  {
    "path": "autospeed/__init__.py",
    "content": "# Find your printers max speed before losing steps\n#\n# Copyright (C) 2024 Anonoei <dev@anonoei.com>\n#\n# This file may be distributed under the terms of the MIT license.\n\nfrom .funcs import *\nfrom .move import *\nfrom .wrappers import *\n\nfrom .main import AutoSpeed"
  },
  {
    "path": "autospeed/funcs.py",
    "content": "# Find your printers max speed before losing steps\n#\n# Copyright (C) 2024 Anonoei <dev@anonoei.com>\n#\n# This file may be distributed under the terms of the MIT license.\n\nimport math\n\ndef calculate_velocity(accel: float, travel: float):\n    return math.sqrt(travel/accel)*accel\n\ndef calculate_accel(veloc: float, travel: float):\n    return veloc**2/travel\n\ndef calculate_distance(veloc: float, accel: float):\n    return veloc**2/accel\n\ndef calculate_diagonal(x: float, y: float):\n    return math.sqrt(x**2 + y**2)\n\ndef calculate_graph(velocity: float, slope: int):\n    return (10000/(velocity/slope))"
  },
  {
    "path": "autospeed/main.py",
    "content": "# Find your printers max speed before losing steps\n#\n# Copyright (C) 2024 Anonoei <dev@anonoei.com>\n#\n# This file may be distributed under the terms of the MIT license.\n\nimport os\nfrom time import perf_counter\nimport datetime as dt\n\nfrom .funcs import calculate_graph, calculate_accel, calculate_velocity\nfrom .move import Move, MoveX, MoveY, MoveZ, MoveDiagX, MoveDiagY\nfrom .wrappers import ResultsWrapper, AttemptWrapper\n\nclass AutoSpeed:\n    def __init__(self, config):\n        self.config = config\n        self.printer = config.get_printer()\n        self.gcode = self.printer.lookup_object('gcode')\n        self.gcode_move = self.printer.load_object(config, 'gcode_move')\n\n        self.printer_kinematics = self.config.getsection(\"printer\").get(\"kinematics\")\n        self.isolate_xy = self.printer_kinematics == 'cartesian' or self.printer_kinematics == 'corexz'\n\n        self.valid_axes = [\"x\", \"y\", \"diag_x\", \"diag_y\", \"z\"]\n        self.axes = self._parse_axis(config.get('axis', 'x, y' if self.isolate_xy else 'diag_x, diag_y'))\n\n        self.default_axes = ''\n\n        for axis in self.axes:\n            self.default_axes += f\"{axis},\"\n        self.default_axes = self.default_axes[:-1]\n\n        self.margin          = config.getfloat(  'margin',          default=20.0, above=0.0)\n        self.settling_home   = config.getboolean('settling_home',   default=True)\n        self.max_missed      = config.getfloat(  'max_missed',      default=1.0)\n        self.endstop_samples = config.getint(    'endstop_samples', default=3, minval=2)\n\n        self.accel_min  = config.getfloat('accel_min',  default=1000.0, above=1.0)\n        self.accel_max  = config.getfloat('accel_max',  default=100000.0, above=self.accel_min)\n        self.accel_accu = config.getfloat('accel_accu', default=0.05, above=0.0, below=1.0)\n        self.scv        = config.getfloat('scv', default=5, above=1.0, below=50)\n\n        self.veloc_min  = config.getfloat('velocity_min',  default=50.0, above=1.0)\n        self.veloc_max  = config.getfloat('velocity_max',  default=5000.0, above=self.veloc_min)\n        self.veloc_accu = config.getfloat('velocity_accu', default=0.05, above=0.0, below=1.0)\n\n        self.derate = config.getfloat('derate', default=0.8, above=0.0, below=1.0)\n\n        self.validate_margin       = config.getfloat('validate_margin', default=self.margin, above=0.0)\n        self.validate_inner_margin = config.getfloat('validate_inner_margin', default=20.0, above=0.0)\n        self.validate_iterations   = config.getint(  'validate_iterations', default=50, minval=1)\n\n        results_default = os.path.expanduser('~')\n        for path in ( # Could be problematic if neither of these paths work\n            os.path.dirname(self.printer.start_args['log_file']),\n            os.path.expanduser('~/printer_data/config'),\n            ):\n            if os.path.exists(path):\n                results_default = path\n        self.results_dir = os.path.expanduser(config.get('results_dir',default=results_default))\n\n        self.toolhead = None\n        self.printer.register_event_handler(\"klippy:connect\", self.handle_connect)\n        self.printer.register_event_handler(\"homing:home_rails_end\", self.handle_home_rails_end)\n\n        self.gcode.register_command('AUTO_SPEED',\n                                    self.cmd_AUTO_SPEED,\n                                    desc=self.cmd_AUTO_SPEED_help)\n        self.gcode.register_command('AUTO_SPEED_VELOCITY',\n                                    self.cmd_AUTO_SPEED_VELOCITY,\n                                    desc=self.cmd_AUTO_SPEED_VELOCITY_help)\n        self.gcode.register_command('AUTO_SPEED_ACCEL',\n                                    self.cmd_AUTO_SPEED_ACCEL,\n                                    desc=self.cmd_AUTO_SPEED_ACCEL_help)\n        self.gcode.register_command('AUTO_SPEED_VALIDATE',\n                                    self.cmd_AUTO_SPEED_VALIDATE,\n                                    desc=self.cmd_AUTO_SPEED_VALIDATE_help)\n        self.gcode.register_command('AUTO_SPEED_GRAPH',\n                                    self.cmd_AUTO_SPEED_GRAPH,\n                                    desc=self.cmd_AUTO_SPEED_GRAPH_help)\n        self.gcode.register_command('X_ENDSTOP_ACCURACY',\n                                    self.cmd_X_ENDSTOP_ACCURACY,\n                                    desc=self.cmd_AUTO_SPEED_GRAPH_help)\n        self.gcode.register_command('Y_ENDSTOP_ACCURACY',\n                                    self.cmd_Y_ENDSTOP_ACCURACY,\n                                    desc=self.cmd_AUTO_SPEED_GRAPH_help)\n        self.gcode.register_command('Z_ENDSTOP_ACCURACY',\n                                    self.cmd_Z_ENDSTOP_ACCURACY,\n                                    desc=self.cmd_AUTO_SPEED_GRAPH_help)\n\n        self.level = None\n\n        self.steppers = {}\n        self.axis_limits = {}\n\n    def handle_connect(self):\n        self.toolhead = self.printer.lookup_object('toolhead')\n        # Reduce speed/acceleration for positioning movement\n        self.th_accel = self.toolhead.max_accel/2\n        self.th_veloc = self.toolhead.max_velocity/2\n        self.th_scv = self.toolhead.square_corner_velocity\n\n        # Find and define leveling method\n        if self.printer.lookup_object(\"screw_tilt_adjust\", None) is not None:\n            self.level = \"STA\"\n        elif self.printer.lookup_object(\"z_tilt\", None) is not None:\n            self.level= \"ZT\"\n        elif self.printer.lookup_object(\"quad_gantry_level\", None) is not None:\n            self.level = \"QGL\"\n        else:\n            self.level = None\n\n    def handle_home_rails_end(self, homing_state, rails):\n        # Get axis min/max values\n        # Get stepper microsteps\n        if not len(self.steppers.keys()) == 3:\n            for rail in rails:\n                pos_min, pos_max = rail.get_range()\n                for stepper in rail.get_steppers():\n                    name = stepper._name\n                    # microsteps = (stepper._steps_per_rotation / full_steps / gearing)\n                    if name in [\"stepper_x\", \"stepper_y\", \"stepper_z\"]:\n                        config = self.printer.lookup_object('configfile').status_raw_config[name]\n                        microsteps = int(config[\"microsteps\"])\n\n                        homing_retract_dist = config.get(\"homing_retract_dist\", None)\n                        if homing_retract_dist is None:\n                            homing_retract_dist = 5 # This shouldn't be hardcoded\n                        homing_retract_dist = float(homing_retract_dist)\n                        second_homing_speed = config.get(\"second_homing_speed\", None)\n                        if second_homing_speed is None:\n                            second_homing_speed = 5 # This shouldn't be hardcoded\n                        second_homing_speed = float(second_homing_speed)\n                        self.steppers[name[-1]] = [pos_min, pos_max, microsteps, homing_retract_dist, second_homing_speed]\n\n            if self.steppers.get(\"x\", None) is not None:\n                self.axis_limits[\"x\"] = {\n                    \"min\": self.steppers[\"x\"][0],\n                    \"max\": self.steppers[\"x\"][1],\n                    \"center\": (self.steppers[\"x\"][0] + self.steppers[\"x\"][1]) / 2,\n                    \"dist\": self.steppers[\"x\"][1] - self.steppers[\"x\"][0],\n                    \"home\": self.gcode_move.homing_position[0]\n                }\n            if self.steppers.get(\"y\", None) is not None:\n                self.axis_limits[\"y\"] = {\n                    \"min\": self.steppers[\"y\"][0],\n                    \"max\": self.steppers[\"y\"][1],\n                    \"center\": (self.steppers[\"y\"][0] + self.steppers[\"y\"][1]) / 2,\n                    \"dist\": self.steppers[\"y\"][1] - self.steppers[\"y\"][0],\n                    \"home\": self.gcode_move.homing_position[1]\n                }\n            if self.steppers.get(\"z\", None) is not None:\n                self.axis_limits[\"z\"] = {\n                    \"min\": self.steppers[\"z\"][0],\n                    \"max\": self.steppers[\"z\"][1],\n                    \"center\": (self.steppers[\"z\"][0] + self.steppers[\"z\"][1]) / 2,\n                    \"dist\": self.steppers[\"z\"][1] - self.steppers[\"z\"][0],\n                    \"home\": self.gcode_move.homing_position[2]\n                }\n\n    cmd_AUTO_SPEED_help = (\"Automatically find your printer's maximum acceleration/velocity\")\n    def cmd_AUTO_SPEED(self, gcmd):\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n\n        validate = gcmd.get_int('VALIDATE', 0, minval=0, maxval=1)\n\n        self._prepare(gcmd) # Make sure the printer is level, [check endstop variance]\n\n        move_z = gcmd.get_int('Z', None)\n        if move_z is not None:\n            self._move([None, None, move_z], self.th_veloc)\n\n        start = perf_counter()\n        accel_results = self.cmd_AUTO_SPEED_ACCEL(gcmd)\n        veloc_results = self.cmd_AUTO_SPEED_VELOCITY(gcmd)\n\n        respond = f\"AUTO SPEED found recommended acceleration and velocity after {perf_counter() - start:.2f}s\\n\"\n        for axis in self.valid_axes:\n            aR = accel_results.vals.get(axis, None)\n            vR = veloc_results.vals.get(axis, None)\n            if aR is not None or vR is not None:\n                respond += f\"| {axis.replace('_', ' ').upper()} max:\"\n                if aR is not None:\n                    respond += f\" a{aR:.0f}\"\n                if vR is not None:\n                    respond += f\" v{vR:.0f}\"\n                respond += \"\\n\"\n\n        respond += f\"Recommended accel: {accel_results.vals['rec']:.0f}\\n\"\n        respond += f\"Recommended velocity: {veloc_results.vals['rec']:.0f}\\n\"\n        self.gcode.respond_info(respond)\n\n        if validate:\n            gcmd._params[\"ACCEL\"] = accel_results.vals['rec']\n            gcmd._params[\"VELOCITY\"] = veloc_results.vals['rec']\n            self.cmd_AUTO_SPEED_VALIDATE(gcmd)\n\n    cmd_AUTO_SPEED_ACCEL_help = (\"Automatically find your printer's maximum acceleration\")\n    def cmd_AUTO_SPEED_ACCEL(self, gcmd):\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n        axes = self._parse_axis(gcmd.get(\"AXIS\", self._axis_to_str(self.axes)))\n\n        margin         = gcmd.get_float(\"MARGIN\", self.margin, above=0.0)\n        derate         = gcmd.get_float('DERATE', self.derate, above=0.0, below=1.0)\n        max_missed      = gcmd.get_float('MAX_MISSED', self.max_missed, above=0.0)\n\n        accel_min  = gcmd.get_float('ACCEL_MIN', self.accel_min, above=1.0)\n        accel_max  = gcmd.get_float('ACCEL_MAX', self.accel_max, above=accel_min)\n        accel_accu = gcmd.get_float('ACCEL_ACCU', self.accel_accu, above=0.0, below=1.0)\n\n        veloc = gcmd.get_float('VELOCITY', 1.0, above=1.0)\n        scv =   gcmd.get_float('SCV', self.scv, above=1.0)\n\n        respond = \"AUTO SPEED finding maximum acceleration on\"\n        for axis in axes:\n            respond += f\" {axis.upper().replace('_', ' ')},\"\n        self.gcode.respond_info(respond[:-1])\n\n        rw = ResultsWrapper()\n        start = perf_counter()\n        for axis in axes:\n            aw = AttemptWrapper()\n            aw.type = \"accel\"\n            aw.accuracy = accel_accu\n            aw.max_missed = max_missed\n            aw.margin = margin\n\n            aw.min = accel_min\n            aw.max  = accel_max\n            aw.veloc = veloc\n            aw.scv = scv\n            self.init_axis(aw, axis)\n            rw.vals[aw.axis] = self.binary_search(aw)\n        rw.duration = perf_counter() - start\n\n        rw.name = \"acceleration\"\n        respond = f\"AUTO SPEED found maximum acceleration after {rw.duration:.2f}s\\n\"\n        for axis in self.valid_axes:\n            if rw.vals.get(axis, None) is not None:\n                respond += f\"| {axis.replace('_', ' ').upper()} max: {rw.vals[axis]:.0f}\\n\"\n        respond += f\"\\n\"\n\n        rw.derate(derate)\n        respond += f\"Recommended values:\\n\"\n        for axis in self.valid_axes:\n            if rw.vals.get(axis, None) is not None:\n                respond += f\"| {axis.replace('_', ' ').upper()} max: {rw.vals[axis]:.0f}\\n\"\n        respond += f\"Recommended acceleration: {rw.vals['rec']:.0f}\\n\"\n\n        self.gcode.respond_info(respond)\n        return rw\n\n    cmd_AUTO_SPEED_VELOCITY_help = (\"Automatically find your printer's maximum velocity\")\n    def cmd_AUTO_SPEED_VELOCITY(self, gcmd):\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n        axes = self._parse_axis(gcmd.get(\"AXIS\", self._axis_to_str(self.axes)))\n\n        margin         = gcmd.get_float(\"MARGIN\", self.margin, above=0.0)\n        derate         = gcmd.get_float('DERATE', self.derate, above=0.0, below=1.0)\n        max_missed      = gcmd.get_float('MAX_MISSED', self.max_missed, above=0.0)\n\n        veloc_min  = gcmd.get_float('VELOCITY_MIN', self.veloc_min, above=1.0)\n        veloc_max  = gcmd.get_float('VELOCITY_MAX', self.veloc_max, above=veloc_min)\n        veloc_accu = gcmd.get_float('VELOCITY_ACCU', self.veloc_accu, above=0.0, below=1.0)\n\n        accel = gcmd.get_float('ACCEL', 1.0, above=1.0)\n        scv =   gcmd.get_float('SCV', self.scv, above=1.0)\n\n        respond = \"AUTO SPEED finding maximum velocity on\"\n        for axis in axes:\n            respond += f\" {axis.upper().replace('_', ' ')},\"\n        self.gcode.respond_info(respond[:-1])\n\n        rw = ResultsWrapper()\n        start = perf_counter()\n        for axis in axes:\n            aw = AttemptWrapper()\n            aw.type = \"velocity\"\n            aw.accuracy  = veloc_accu\n            aw.max_missed = max_missed\n            aw.margin = margin\n\n            aw.min = veloc_min\n            aw.max  = veloc_max\n            aw.accel = accel\n            aw.scv = scv\n            self.init_axis(aw, axis)\n            rw.vals[aw.axis] = self.binary_search(aw)\n        rw.duration = perf_counter() - start\n\n        rw.name = \"velocity\"\n        respond = f\"AUTO SPEED found maximum velocity after {rw.duration:.2f}s\\n\"\n        for axis in self.valid_axes:\n            if rw.vals.get(axis, None) is not None:\n                respond += f\"| {axis.replace('_', ' ').upper()} max: {rw.vals[axis]:.0f}\\n\"\n        respond += \"\\n\"\n\n        rw.derate(derate)\n        respond += f\"Recommended values\\n\"\n        for axis in self.valid_axes:\n            if rw.vals.get(axis, None) is not None:\n                respond += f\"| {axis.replace('_', ' ').upper()} max: {rw.vals[axis]:.0f}\\n\"\n        respond += f\"Recommended velocity: {rw.vals['rec']:.0f}\\n\"\n\n        self.gcode.respond_info(respond)\n        return rw\n\n    cmd_AUTO_SPEED_VALIDATE_help = (\"Validate your printer's acceleration/velocity don't miss steps\")\n    def cmd_AUTO_SPEED_VALIDATE(self, gcmd):\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n\n        max_missed   = gcmd.get_float('MAX_MISSED', self.max_missed, above=0.0)\n        margin       = gcmd.get_float('VALIDATE_MARGIN', default=self.validate_margin, above=0.0)\n        small_margin = gcmd.get_float('VALIDATE_INNER_MARGIN', default=self.validate_inner_margin, above=0.0)\n        iterations   = gcmd.get_int('VALIDATE_ITERATIONS', default=self.validate_iterations, minval=1)\n\n        accel = gcmd.get_float('ACCEL', default=self.toolhead.max_accel, above=0.0)\n        veloc = gcmd.get_float('VELOCITY', default=self.toolhead.max_velocity, above=0.0)\n        scv =   gcmd.get_float('SCV', default=self.toolhead.square_corner_velocity, above=1.0)\n\n        respond = f\"AUTO SPEED validating over {iterations} iterations\\n\"\n        respond += f\"Acceleration: {accel:.0f}\\n\"\n        respond += f\"Velocity: {veloc:.0f}\\n\"\n        respond += f\"SCV: {scv:.0f}\"\n        self.gcode.respond_info(respond)\n        self._set_velocity(veloc, accel, scv)\n        valid, duration, missed_x, missed_y = self._validate(veloc, iterations, margin, small_margin, max_missed)\n\n        respond = f\"AUTO SPEED validated results after {duration:.2f}s\\n\"\n        respond += f\"Valid: {valid}\\n\"\n        respond += f\"Missed X {missed_x:.2f}, Y {missed_y:.2f}\"\n        self.gcode.respond_info(respond)\n        return valid\n\n    cmd_AUTO_SPEED_GRAPH_help = (\"Graph your printer's maximum acceleration at given velocities\")\n    def cmd_AUTO_SPEED_GRAPH(self, gcmd):\n        import matplotlib.pyplot as plt # this may fail if matplotlib isn't installed\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n        axes = self._parse_axis(gcmd.get(\"AXIS\", self._axis_to_str(self.axes)))\n\n        margin     = gcmd.get_float(\"MARGIN\", self.margin, above=0.0)\n        derate     = gcmd.get_float('DERATE', self.derate, above=0.0, below=1.0)\n        max_missed = gcmd.get_float('MAX_MISSED', self.max_missed, above=0.0)\n        \n        scv        = gcmd.get_float('SCV', default=self.toolhead.square_corner_velocity, above=1.0)\n\n        veloc_min  = gcmd.get_float('VELOCITY_MIN', 200.0, above=0.0)\n        veloc_max  = gcmd.get_float('VELOCITY_MAX', 700.0, above=veloc_min)\n        veloc_div  = gcmd.get_int(  'VELOCITY_DIV', 5, minval=0)\n\n        accel_accu = gcmd.get_float('ACCEL_ACCU', 0.05, above=0.0, below=1.0)\n\n        accel_min_slope = gcmd.get_int('ACCEL_MIN_SLOPE', 100, minval=0)\n        accel_max_slope = gcmd.get_int('ACCEL_MAX_SLOPE', 1800, minval=accel_min_slope)\n\n        veloc_step = (veloc_max - veloc_min)//(veloc_div - 1)\n        velocs = [round((v * veloc_step) + veloc_min) for v in range(0, veloc_div)]\n        respond = \"AUTO SPEED graphing maximum accel from velocities on\"\n        for axis in axes:\n            respond += f\" {axis.upper().replace('_', ' ')},\"\n        respond = respond[:-1] + \"\\n\"\n        respond += f\"V_MIN: {veloc_min}, V_MAX: {veloc_max}, V_STEP: {veloc_step}\\n\"\n        self.gcode.respond_info(respond)\n\n        aw = AttemptWrapper()\n        aw.type = \"graph\"\n        aw.accuracy = accel_accu\n        aw.max_missed = max_missed\n        aw.margin = margin\n        aw.scv = scv\n        for axis in axes:\n            start = perf_counter()\n            self.init_axis(aw, axis)\n            accels = []\n            accel_mins = []\n            accel_maxs = []\n            for veloc in velocs:\n                self.gcode.respond_info(f\"AUTO SPEED graph {aw.axis} - v{veloc}\")\n                aw.veloc = veloc\n                aw.min = round(calculate_graph(veloc, accel_min_slope))\n                aw.max = round(calculate_graph(veloc, accel_max_slope))\n                accel_mins.append(aw.min)\n                accel_maxs.append(aw.max)\n                accels.append(self.binary_search(aw))\n            plt.plot(velocs, accels, 'go-', label='measured')\n            plt.plot(velocs, [a*derate for a in accels], 'g-', label='derated')\n            plt.plot(velocs, accel_mins, 'b--', label='min')\n            plt.plot(velocs, accel_maxs, 'r--', label='max')\n            plt.legend(loc='upper right')\n            plt.title(f\"Max accel at velocity on {aw.axis} to {int(accel_accu*100)}% accuracy\")\n            plt.xlabel(\"Velocity\")\n            plt.ylabel(\"Acceleration\")\n            filepath = os.path.join(\n                self.results_dir,\n                f\"AUTO_SPEED_GRAPH_{dt.datetime.now():%Y-%m-%d_%H:%M:%S}_{aw.axis}.png\"\n            )\n            self.gcode.respond_info(f\"Velocs: {velocs}\")\n            self.gcode.respond_info(f\"Accels: {accels}\")\n            self.gcode.respond_info(f\"AUTO SPEED graph found max accel on {aw.axis} after {perf_counter() - start:.0f}s\\nSaving graph to {filepath}\")\n            os.makedirs(self.results_dir, exist_ok=True)\n            plt.savefig(filepath, bbox_inches='tight')\n            plt.close()\n\n    # -------------------------------------------------------\n    #\n    #     Internal Helpers\n    #\n    # -------------------------------------------------------\n    def _prepare(self, gcmd):\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n\n        start = perf_counter()\n        # Level the printer if it's not leveled\n        self._level(gcmd)\n        self._move([self.axis_limits[\"x\"][\"center\"], self.axis_limits[\"y\"][\"center\"], self.axis_limits[\"z\"][\"center\"]], self.th_veloc)\n\n        self._variance(gcmd)\n\n        return perf_counter() - start\n\n    def _level(self, gcmd):\n        level = gcmd.get_int('LEVEL', 1, minval=0, maxval=1)\n\n        if level == 0:\n            return\n        if self.level is None:\n            return\n\n        lookup = None\n        name = None\n        if self.level == \"STA\":\n            lookup = \"screw_tilt_adjust\"\n            name = \"SCREWS_TILT_CALCULATE\"\n        elif self.level == \"ZT\":\n            lookup = \"z_tilt\"\n            name = \"Z_TILT_ADJUST\"\n        elif self.level == \"QGL\":\n            lookup = \"quad_gantry_level\"\n            name = \"QUAD_GANTRY_LEVEL\"\n        else:\n            raise gcmd.error(f\"Unknown leveling method '{self.level}'.\")\n        lm = self.printer.lookup_object(lookup)\n        if lm.z_status.applied is False:\n            self.gcode.respond_info(f\"AUTO SPEED leveling with {name}...\")\n            self.gcode._process_commands([name], False)\n            if lm.z_status.applied is False:\n                raise gcmd.error(f\"Failed to level printer! Please manually ensure your printer is level.\")\n\n    def _variance(self, gcmd):\n        variance        = gcmd.get_int('VARIANCE', 1, minval=0, maxval=1)\n\n        max_missed      = gcmd.get_float('MAX_MISSED', self.max_missed, above=0.0)\n        endstop_samples = gcmd.get_int('ENDSTOP_SAMPLES', self.endstop_samples, minval=2)\n\n        settling_home   = gcmd.get_int(\"SETTLING_HOME\", default=self.settling_home, minval=0, maxval=1)\n\n        if variance == 0:\n            return\n\n        self.gcode.respond_info(f\"AUTO SPEED checking endstop variance over {endstop_samples} samples\")\n\n        if settling_home:\n            self.toolhead.wait_moves()\n            self._home(True, True, False)\n\n        axes = self._parse_axis(gcmd.get(\"AXIS\", self._axis_to_str(self.axes)))\n\n        check_x = 'x' in axes if self.isolate_xy else True\n        check_y = 'y' in axes if self.isolate_xy else True\n\n        # Check endstop variance\n        endstops = self._endstop_variance(endstop_samples, x=check_x, y=check_y)\n\n        x_max = max(endstops[\"x\"]) if check_x else 0\n        y_max = max(endstops[\"y\"]) if check_y else 0\n        self.gcode.respond_info(f\"AUTO SPEED endstop variance:\\nMissed X:{x_max:.2f} steps, Y:{y_max:.2f} steps\")\n\n        if x_max >= max_missed or y_max >= max_missed:\n            raise gcmd.error(f\"Please increase MAX_MISSED (currently {max_missed}), or tune your steppers/homing macro.\")\n\n    # -------------------------------------------------------\n    #\n    #     Internal Methods\n    #\n    # -------------------------------------------------------\n    def _parse_axis(self, raw_axes):\n        raw_axes = raw_axes.lower()\n        raw_axes = raw_axes.replace(\" \", \"\")\n        raw_axes = raw_axes.split(',')\n        axes = []\n        for axis in raw_axes:\n            if axis in self.valid_axes:\n                axes.append(axis)\n        return axes\n\n    def _axis_to_str(self, raw_axes):\n        axes = \"\"\n        for axis in raw_axes:\n            axes += f\"{axis},\"\n        axes = axes[:-1]\n        return axes\n\n    def init_axis(self, aw: AttemptWrapper, axis):\n        aw.axis = axis\n        if axis == \"diag_x\":\n            aw.move = MoveDiagX()\n        elif axis == \"diag_y\":\n            aw.move = MoveDiagY()\n        elif axis == \"x\":\n            aw.move = MoveX()\n        elif axis == \"y\":\n            aw.move = MoveY()\n        elif axis == \"z\":\n            aw.move = MoveZ()\n        aw.move.Init(self.axis_limits, aw.margin, self.isolate_xy)\n\n    def binary_search(self, aw: AttemptWrapper):\n        aw.time_start = perf_counter()\n        m_min = aw.min\n        m_max = aw.max\n        m_var = m_min + (m_max-m_min) // 3\n\n        if aw.veloc == 0.0:\n            aw.veloc = 1.0\n        if aw.accel == 0.0:\n            aw.accel = 1.0\n\n        if aw.type in (\"accel\", \"graph\"): # stat is velocity, var is accel\n            m_stat = aw.veloc\n            o_veloc = aw.veloc\n            if o_veloc == 1.0:\n                aw.accel = calculate_accel(aw.veloc, aw.move.max_dist)\n            aw.move.Calc(self.axis_limits, m_stat, m_var, aw.margin)\n\n        elif aw.type in (\"velocity\"): # stat is accel, var is velocity\n            m_stat = aw.accel\n            o_accel = aw.accel\n            if o_accel == 1.0:\n                aw.veloc = calculate_velocity(aw.accel, aw.move.max_dist)\n            aw.move.Calc(self.axis_limits, m_var, m_stat, aw.margin)\n\n        measuring = True\n        measured_val = None\n        aw.tries = 0\n        aw.home_steps, aw.move_time_prehome = self._prehome(aw.move.home)\n        while measuring:\n            aw.tries += 1\n            if aw.type in (\"accel\", \"graph\"):\n                if o_veloc == 1.0:\n                    m_stat = aw.veloc = calculate_velocity(m_var, aw.move.dist)/2.5\n                aw.accel = m_var\n                aw.move.Calc(self.axis_limits, m_stat, m_var, aw.margin)\n            elif aw.type == \"velocity\":\n                if o_accel == 1.0:\n                    m_stat = aw.accel = calculate_accel(m_var, aw.move.dist)*2.5\n                aw.veloc = m_var\n                aw.move.Calc(self.axis_limits, m_var, m_stat, aw.margin)\n            #self.gcode.respond_info(str(aw))\n\n            valid = self._attempt(aw)\n\n            if aw.type in (\"accel\", \"graph\"):\n                veloc = m_stat\n                accel = m_var\n            elif aw.type in (\"velocity\"):\n                veloc = m_var\n                accel = m_stat\n            respond = f\"AUTO SPEED {aw.type} on {aw.axis} try {aw.tries} ({aw.time_last:.2f}s)\\n\"\n            respond += f\"Moved {aw.move_dist - aw.margin:.2f}mm at a{accel:.0f}/v{veloc:.0f} after {aw.move_time_prehome:.2f}/{aw.move_time:.2f}/{aw.move_time_posthome:.2f}s\\n\"\n            respond += f\"Missed\"\n            if aw.move.home[0]:\n                respond += f\" X {aw.missed['x']:.2f},\"\n            if aw.move.home[1]:\n                respond += f\" Y {aw.missed['y']:.2f},\"\n            if aw.move.home[2]:\n                respond += f\" Z {aw.missed['z']:.2f},\"\n            self.gcode.respond_info(respond[:-1])\n            if measured_val is not None:\n                if m_var * (1 + aw.accuracy) > m_max or m_var * (1 - aw.accuracy) < m_min:\n                    measuring = False\n            measured_val = m_var\n            if valid:\n                m_min = m_var\n            else:\n                m_max = m_var\n            m_var = (m_min + m_max)//2\n\n        aw.time_total = perf_counter() - aw.time_start\n        return m_var\n\n    def _attempt(self, aw: AttemptWrapper):\n        timeAttempt = perf_counter()\n\n        self._set_velocity(self.th_veloc, self.th_accel, self.th_scv)\n        self._move([aw.move.pos[\"x\"][0], aw.move.pos[\"y\"][0], aw.move.pos[\"z\"][0]], self.th_veloc)\n        self.toolhead.wait_moves()\n        self._set_velocity(aw.veloc, aw.accel, aw.scv)\n        timeMove = perf_counter()\n\n        self._move([aw.move.pos[\"x\"][1], aw.move.pos[\"y\"][1], aw.move.pos[\"z\"][1]], aw.veloc)\n        self.toolhead.wait_moves()\n        aw.move_time = perf_counter() - timeMove\n        aw.move_dist = aw.move.dist\n\n        valid, aw.home_steps, aw.missed, aw.move_time_posthome = self._posttest(aw.home_steps, aw.max_missed, aw.move.home)\n        aw.time_last = perf_counter() - timeAttempt\n        return valid\n\n    def _validate(self, speed, iterations, margin, small_margin, max_missed):\n        pos = {\n            \"x\": {\n                \"min\": self.axis_limits[\"x\"][\"min\"] + margin,\n                \"max\": self.axis_limits[\"x\"][\"max\"] - margin,\n                \"center_min\": self.axis_limits[\"x\"][\"center\"] - (small_margin/2),\n                \"center_max\": self.axis_limits[\"x\"][\"center\"] + (small_margin/2),\n            },\n            \"y\": {\n                \"min\": self.axis_limits[\"y\"][\"min\"] + margin,\n                \"max\": self.axis_limits[\"y\"][\"max\"] - margin,\n                \"center_min\": self.axis_limits[\"y\"][\"center\"] - (small_margin/2),\n                \"center_max\": self.axis_limits[\"y\"][\"center\"] + (small_margin/2),\n            }\n        }\n        self.toolhead.wait_moves()\n        self._home(True, True, False)\n        start_steps = self._get_steps()\n        start = perf_counter()\n        for _ in range(iterations):\n            self._move([pos[\"x\"][\"min\"], pos[\"y\"][\"min\"], None], speed)\n            self._move([pos[\"x\"][\"max\"], pos[\"y\"][\"max\"], None], speed)\n            self._move([pos[\"x\"][\"min\"], pos[\"y\"][\"min\"], None], speed)\n            self._move([pos[\"x\"][\"max\"], pos[\"y\"][\"min\"], None], speed)\n            self._move([pos[\"x\"][\"min\"], pos[\"y\"][\"max\"], None], speed)\n            self._move([pos[\"x\"][\"max\"], pos[\"y\"][\"min\"], None], speed)\n\n            # Large pattern box\n            self._move([pos[\"x\"][\"min\"], pos[\"y\"][\"min\"], None], speed)\n            self._move([pos[\"x\"][\"min\"], pos[\"y\"][\"max\"], None], speed)\n            self._move([pos[\"x\"][\"max\"], pos[\"y\"][\"max\"], None], speed)\n            self._move([pos[\"x\"][\"max\"], pos[\"y\"][\"min\"], None], speed)\n\n            # Small pattern diagonals\n            self._move([pos[\"x\"][\"center_min\"], pos[\"y\"][\"center_min\"], None], speed)\n            self._move([pos[\"x\"][\"center_max\"], pos[\"y\"][\"center_max\"], None], speed)\n            self._move([pos[\"x\"][\"center_min\"], pos[\"y\"][\"center_min\"], None], speed)\n            self._move([pos[\"x\"][\"center_max\"], pos[\"y\"][\"center_min\"], None], speed)\n            self._move([pos[\"x\"][\"center_min\"], pos[\"y\"][\"center_max\"], None], speed)\n            self._move([pos[\"x\"][\"center_max\"], pos[\"y\"][\"center_min\"], None], speed)\n\n            # Small pattern box\n            self._move([pos[\"x\"][\"center_min\"], pos[\"y\"][\"center_min\"], None], speed)\n            self._move([pos[\"x\"][\"center_min\"], pos[\"y\"][\"center_max\"], None], speed)\n            self._move([pos[\"x\"][\"center_max\"], pos[\"y\"][\"center_max\"], None], speed)\n            self._move([pos[\"x\"][\"center_max\"], pos[\"y\"][\"center_min\"], None], speed)\n\n        self.toolhead.wait_moves()\n        duration = perf_counter() - start\n\n        self._home(True, True, False)\n        stop_steps = self._get_steps()\n\n\n        step_dif = {\n            \"x\": abs(start_steps[\"x\"] - stop_steps[\"x\"]),\n            \"y\": abs(start_steps[\"y\"] - stop_steps[\"y\"])\n        }\n\n        missed_x = step_dif['x']/self.steppers['x'][2]\n        missed_y = step_dif['y']/self.steppers['y'][2]\n        valid = True\n        if missed_x > max_missed:\n            valid = False\n        if missed_y > max_missed:\n            valid = False\n        return valid, duration, missed_x, missed_y\n\n    def _endstop_variance(self, samples: int, x=True, y=True):\n        variance = {\n            \"x\": [],\n            \"y\": [],\n            \"steps\": {\n                \"x\": None,\n                \"y\": None\n            }\n        }\n        for _ in range(0, samples):\n            self.toolhead.wait_moves()\n            self._home(x, y, False)\n            steps = self._get_steps()\n\n            if x:\n                if variance[\"steps\"][\"x\"] is not None:\n                    x_dif = abs(variance[\"steps\"][\"x\"] - steps[\"x\"])\n                    missed_x = x_dif/self.steppers['x'][2]\n                    variance[\"x\"].append(missed_x)\n                variance[\"steps\"][\"x\"] = steps[\"x\"]\n            if y:\n                if variance[\"steps\"][\"y\"] is not None:\n                    y_dif = abs(variance[\"steps\"][\"y\"] - steps[\"y\"])\n                    missed_y = y_dif/self.steppers['y'][2]\n                    variance[\"y\"].append(missed_y)\n                variance[\"steps\"][\"y\"] = steps[\"y\"]\n        return variance\n\n    def _move(self, coord, speed):\n        self.toolhead.manual_move(coord, speed)\n\n    def _home(self, x=True, y=True, z=True):\n        prevAccel = self.toolhead.max_accel\n        prevVeloc = self.toolhead.max_velocity\n        prevScv   = self.toolhead.square_corner_velocity\n        self._set_velocity(self.th_veloc, self.th_accel, self.th_scv)\n        command = [\"G28\"]\n        if x:\n            command[-1] += \" X0\"\n        if y:\n            command[-1] += \" Y0\"\n        if z:\n            command[-1] += \" Z0\"\n        self.gcode._process_commands(command, False)\n        self.toolhead.wait_moves()\n        self._set_velocity(prevVeloc, prevAccel, prevScv)\n\n    def _get_steps(self):\n        kin = self.toolhead.get_kinematics()\n        steppers = kin.get_steppers()\n        pos = {}\n        for s in steppers:\n            s_name = s.get_name()\n            if s_name in [\"stepper_x\", \"stepper_y\", \"stepper_z\"]:\n                pos[s_name[-1]] = s.get_mcu_position()\n        return pos\n\n    def _prehome(self, home: list):\n        self.toolhead.wait_moves()\n        dur = perf_counter()\n        self._home(home[0], home[1], home[2])\n        self.toolhead.wait_moves()\n        dur = perf_counter() - dur\n\n        home_steps = self._get_steps()\n        return home_steps, dur\n\n    def _posttest(self, start_steps, max_missed, home: list):\n        self.toolhead.wait_moves()\n        dur = perf_counter()\n        self._home(home[0], home[1], home[2])\n        self.toolhead.wait_moves()\n        dur = perf_counter() - dur\n\n        valid = True\n        stop_steps = self._get_steps()\n        step_dif = {}\n        missed = {}\n        if home[0]:\n            step_dif[\"x\"] = abs(start_steps[\"x\"] - stop_steps[\"x\"])\n            missed[\"x\"] = step_dif['x']/self.steppers['x'][2]\n            if missed[\"x\"] > max_missed:\n                valid = False\n        if home[1]:\n            step_dif[\"y\"] = abs(start_steps[\"y\"] - stop_steps[\"y\"])\n            missed[\"y\"] = step_dif['y']/self.steppers['y'][2]\n            if missed[\"y\"] > max_missed:\n                valid = False\n        if home[2]:\n            step_dif[\"z\"] = abs(start_steps[\"z\"] - stop_steps[\"z\"])\n            missed[\"z\"] = step_dif['z']/self.steppers['z'][2]\n            if missed[\"z\"] > max_missed:\n                valid = False\n\n        return valid, stop_steps, missed, dur\n\n    def _set_velocity(self, velocity: float, accel: float, scv: float):\n        #self.gcode.respond_info(f\"AUTO SPEED setting limits to VELOCITY={velocity} ACCEL={accel}\")\n        self.toolhead.max_velocity = velocity\n        self.toolhead.max_accel = accel\n        self.toolhead.requested_accel_to_decel = accel\n        self.toolhead.square_corner_velocity = scv\n        self.toolhead._calc_junction_deviation()\n\n    def cmd_X_ENDSTOP_ACCURACY(self, gcmd):\n\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n\n        # Number of samples for accuracy check\n        sample_count = gcmd.get_int(\"SAMPLES\", 10, minval=1)\n\n        # Retrieve homing parameters for the X axis from the previously stored values\n        second_homing_speed = self.steppers['x'][4]\n        homing_retract_dist = self.steppers['x'][3]\n\n        # Toolhead object to control the movement\n        toolhead = self.printer.lookup_object('toolhead')\n        pos = toolhead.get_position()\n\n        # Log the starting position for X\n        gcmd.respond_info(\"X_ENDSTOP_ACCURACY at X:%.3f (samples=%d)\\n\" % (pos[0], sample_count))\n        gcmd.respond_info(\"Second Homing Speed: %.2f mm/s\" % second_homing_speed)\n        gcmd.respond_info(\"Homing Retract Distance: %.2f mm\" % homing_retract_dist)\n\n\n        # Create a dummy gcode command for a single sample\n        fo_params = dict(gcmd.get_command_parameters())\n        fo_params['SAMPLES'] = '1'\n        gcode = self.printer.lookup_object('gcode')\n        fo_gcmd = gcode.create_gcode_command(\"\", \"\", fo_params)\n\n        # List to store the X positions hit during each sample\n        positions = []\n\n        # Move to the X endstop sample_count times and collect the X positions\n        for _ in range(sample_count):\n            self._home(True, False, False)\n            pos = toolhead.get_position()  # Get the current X position after homing\n            positions.append(pos[0])\n            toolhead.manual_move([pos[0] - homing_retract_dist, None, None], speed=second_homing_speed)  # Move away from the endstop\n\n        # Calculate the maximum, minimum, average, and standard deviation for X positions\n        max_value = max(positions)\n        min_value = min(positions)\n        avg_value = sum(positions) / len(positions)\n        range_value = max_value - min_value\n\n        deviation_sum = sum([(x - avg_value) ** 2 for x in positions])\n        sigma = (deviation_sum / len(positions)) ** 0.5\n\n        # Display results\n        gcmd.respond_info(\n            \"X endstop accuracy results: maximum %.6f, minimum %.6f, range %.6f, \"\n            \"average %.6f, standard deviation %.6f\" % (max_value, min_value, range_value, avg_value, sigma))\n\n\n    def cmd_Y_ENDSTOP_ACCURACY(self, gcmd):\n\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n\n        # Number of samples for accuracy check\n        sample_count = gcmd.get_int(\"SAMPLES\", 10, minval=1)\n\n        # Retrieve homing parameters for the Y axis from the previously stored values\n        second_homing_speed = self.steppers['y'][4]\n        homing_retract_dist = self.steppers['y'][3]\n\n        # Toolhead object to control the movement\n        toolhead = self.printer.lookup_object('toolhead')\n        pos = toolhead.get_position()\n\n        # Log the starting position for Y\n        gcmd.respond_info(\"Y_ENDSTOP_ACCURACY at Y:%.3f (samples=%d)\\n\" % (pos[1], sample_count))\n        gcmd.respond_info(\"Second Homing Speed: %.2f mm/s\" % second_homing_speed)\n        gcmd.respond_info(\"Homing Retract Distance: %.2f mm\" % homing_retract_dist)\n\n\n        # Create a dummy gcode command for a single sample\n        fo_params = dict(gcmd.get_command_parameters())\n        fo_params['SAMPLES'] = '1'\n        gcode = self.printer.lookup_object('gcode')\n        fo_gcmd = gcode.create_gcode_command(\"\", \"\", fo_params)\n\n        # List to store the Y positions hit during each sample\n        positions = []\n\n        # Move to the Y endstop sample_count times and collect the Y positions\n        for _ in range(sample_count):\n            self._home(False, True, False)\n            pos = toolhead.get_position()  # Get the current Y position after homing\n            positions.append(pos[1])\n            toolhead.manual_move([None, pos[1] - homing_retract_dist, None], speed=second_homing_speed)  # Move away from the endstop\n\n        # Calculate the maximum, minimum, average, and standard deviation for Y positions\n        max_value = max(positions)\n        min_value = min(positions)\n        avg_value = sum(positions) / len(positions)\n        range_value = max_value - min_value\n\n        deviation_sum = sum([(y - avg_value) ** 2 for y in positions])\n        sigma = (deviation_sum / len(positions)) ** 0.5\n\n        # Display results\n        gcmd.respond_info(\n            \"Y endstop accuracy results: maximum %.6f, minimum %.6f, range %.6f, \"\n            \"average %.6f, standard deviation %.6f\" % (max_value, min_value, range_value, avg_value, sigma))\n\n    def cmd_Z_ENDSTOP_ACCURACY(self, gcmd):\n\n        if not len(self.steppers.keys()) == 3:\n            raise gcmd.error(f\"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.\")\n\n        # Number of samples for accuracy check\n        sample_count = gcmd.get_int(\"SAMPLES\", 10, minval=1)\n\n        # Retrieve homing parameters for the Z axis from the previously stored values\n        second_homing_speed = self.steppers['z'][4]\n        homing_retract_dist = self.steppers['z'][3]\n\n        # Toolhead object to control the movement\n        toolhead = self.printer.lookup_object('toolhead')\n        pos = toolhead.get_position()\n\n        # Log the starting position for Z\n        gcmd.respond_info(\"Z_ENDSTOP_ACCURACY at Z:%.3f (samples=%d)\\n\" % (pos[2], sample_count))\n        gcmd.respond_info(\"Second Homing Speed: %.2f mm/s\" % second_homing_speed)\n        gcmd.respond_info(\"Homing Retract Distance: %.2f mm\" % homing_retract_dist)\n\n\n        # Create a dummy gcode command for a single sample\n        fo_params = dict(gcmd.get_command_parameters())\n        fo_params['SAMPLES'] = '1'\n        gcode = self.printer.lookup_object('gcode')\n        fo_gcmd = gcode.create_gcode_command(\"\", \"\", fo_params)\n\n        # List to store the Z positions hit during each sample\n        positions = []\n\n        # Move to the Z endstop sample_count times and collect the Z positions\n        for _ in range(sample_count):\n            self._home(False, False, True)\n            pos = toolhead.get_position()  # Get the current Z position after homing\n            positions.append(pos[2])\n            toolhead.manual_move([None, None, pos[2] + homing_retract_dist], speed=second_homing_speed)  # Move away from the endstop\n\n        # Calculate the maximum, minimum, average, and standard deviation for Z positions\n        max_value = max(positions)\n        min_value = min(positions)\n        avg_value = sum(positions) / len(positions)\n        range_value = max_value - min_value\n\n        deviation_sum = sum([(z - avg_value) ** 2 for z in positions])\n        sigma = (deviation_sum / len(positions)) ** 0.5\n\n        # Display results\n        gcmd.respond_info(\n            \"Z endstop accuracy results: maximum %.6f, minimum %.6f, range %.6f, \"\n            \"average %.6f, standard deviation %.6f\" % (max_value, min_value, range_value, avg_value, sigma))\n"
  },
  {
    "path": "autospeed/move.py",
    "content": "# Find your printers max speed before losing steps\n#\n# Copyright (C) 2024 Anonoei <dev@anonoei.com>\n#\n# This file may be distributed under the terms of the MIT license.\n\nimport math\n\nfrom .funcs import calculate_distance\n\nclass Move:\n    home = [False, False, False]\n    def __init__(self):\n        self.dist = 0.0\n        self.pos = {}\n        self.max_dist: float = 0.0\n\n    def __str__(self):\n        fmt = f\"dist/max {self.dist:.0f}/{self.max_dist:.0f}\\n\"\n        if self.pos.get(\"x\", None) is not None:\n            fmt += f\"Pos X: {self.pos['x']}\\n\"\n        if self.pos.get(\"y\", None) is not None:\n            fmt += f\"Pos Y: {self.pos['y']}\\n\"\n        if self.pos.get(\"z\", None) is not None:\n            fmt += f\"Pos Z: {self.pos['z']}\\n\"\n        return fmt\n\n    def _calc(self, axis_limits, veloc, accel, margin):\n        if self.max_dist == 0.0:\n            self.Init(axis_limits, margin)\n\n    def _validate(self, margin: float):\n        if self.dist < 5.0:\n            self.dist = 5.0\n        self.dist += margin\n        if self.dist > self.max_dist:\n            self.dist = self.max_dist\n\n    def Init(self, axis_limits, margin):\n        ...\n    def Calc(self, axis_limits, veloc, accel, margin):\n        ...\n\nclass MoveX(Move):\n    def Init(self, axis_limits, margin, isolate_xy):\n        home_y = not isolate_xy \n        self.home = [True, home_y, False]\n        self.max_dist = axis_limits[\"x\"][\"dist\"] - margin*2\n    def Calc(self, axis_limits, veloc, accel, margin):\n        self._calc(axis_limits, veloc, accel, margin)\n        self.dist = calculate_distance(veloc, accel)/2\n        self._validate(margin)\n        self.pos = {\n            \"x\": [\n                axis_limits[\"x\"][\"max\"] - self.dist,\n                axis_limits[\"x\"][\"max\"] - margin\n            ],\n            \"y\": [None, None],\n            \"z\": [None, None]\n        }\n\nclass MoveY(Move):\n    def Init(self, axis_limits, margin, isolate_xy):\n        home_x = not isolate_xy \n        self.home = [home_x, True, False]\n        self.max_dist = axis_limits[\"y\"][\"dist\"] - margin*2\n    def Calc(self, axis_limits, veloc, accel, margin):\n        self._calc(axis_limits, veloc, accel, margin)\n        self.dist = calculate_distance(veloc, accel)/2\n        self._validate(margin)\n        self.pos = {\n            \"x\": [None, None],\n            \"y\": [\n                axis_limits[\"y\"][\"max\"] - self.dist,\n                axis_limits[\"y\"][\"max\"] - margin\n            ],\n            \"z\": [None, None]\n        }\n\nclass MoveDiagX(Move):\n    home = [True, True, False]\n    def Init(self, axis_limits, margin, _):\n        self.max_dist = min(axis_limits[\"x\"][\"dist\"], axis_limits[\"y\"][\"dist\"]) - margin*2\n    def Calc(self, axis_limits, veloc, accel, margin):\n        self._calc(axis_limits, veloc, accel, margin)\n        self.dist = (calculate_distance(veloc, accel)/2 * math.sin(45))\n        self._validate(margin)\n        self.pos = {\n            \"x\": [\n                axis_limits[\"x\"][\"max\"] - self.dist,\n                axis_limits[\"x\"][\"max\"] - margin\n            ],\n            \"y\": [\n                axis_limits[\"y\"][\"max\"] - self.dist,\n                axis_limits[\"y\"][\"max\"] - margin\n            ],\n            \"z\": [None, None]\n        }\n\nclass MoveDiagY(Move):\n    home = [True, True, False]\n    def Init(self, axis_limits, margin, _):\n        self.max_dist = min(axis_limits[\"x\"][\"dist\"], axis_limits[\"y\"][\"dist\"]) - margin*2\n    def Calc(self, axis_limits, veloc, accel, margin):\n        self._calc(axis_limits, veloc, accel, margin)\n        self.dist = (calculate_distance(veloc, accel)/2 * math.sin(45))\n        self._validate(margin)\n        self.pos = {\n            \"x\": [\n                axis_limits[\"x\"][\"min\"] + self.dist,\n                axis_limits[\"x\"][\"min\"] + margin\n            ],\n            \"y\": [\n                axis_limits[\"y\"][\"max\"] - self.dist,\n                axis_limits[\"y\"][\"max\"] - margin\n            ],\n            \"z\": [None, None]\n        }\n\nclass MoveZ(Move):\n    home = [False, False, True]\n    def Init(self, axis_limits, margin, _):\n        self.max_dist = axis_limits[\"z\"][\"dist\"] - margin*2\n    def Calc(self, axis_limits, veloc, accel, margin):\n        self.dist = (calculate_distance(veloc, accel))\n        self._validate(margin)\n        self.pos = {\n            \"x\": [None, None],\n            \"y\": [None, None],\n        }\n        if axis_limits[\"z\"][\"home\"] <= axis_limits[\"z\"][\"min\"]:\n            self.pos[\"z\"] =  [\n                axis_limits[\"z\"][\"min\"] + self.dist,\n                axis_limits[\"z\"][\"min\"] + margin\n            ]\n        else:\n            self.pos[\"z\"] =  [\n                axis_limits[\"z\"][\"max\"] - self.dist,\n                axis_limits[\"z\"][\"max\"] - margin\n            ]"
  },
  {
    "path": "autospeed/wrappers.py",
    "content": "# Find your printers max speed before losing steps\n#\n# Copyright (C) 2024 Anonoei <dev@anonoei.com>\n#\n# This file may be distributed under the terms of the MIT license.\n\nfrom .move import Move\n\nclass ResultsWrapper:\n    def __init__(self):\n        self.name: str = \"\"\n        self.duration: float = None\n        self.vals: dict = {}\n\n    def __str__(self):\n        fmt = f\"ResultsWrapper {self.name}, duration: {self.duration}\\n\"\n        fmt += f\"| Vals: {self.vals}\"\n        return fmt\n\n    def derate(self, derate):\n        vList = []\n        newVals = {}\n        for k, v in self.vals.items():\n            newVals[f\"max_{k}\"] = v\n            newVals[k] = v * derate\n            vList.append(newVals[k])\n        self.vals = newVals\n        self.vals[\"rec\"] = min(vList)\n\nclass AttemptWrapper:\n    def __init__(self):\n        self.type: str = \"\"\n        self.axis: str = \"\"\n        self.min: float = None\n        self.max: float = None\n        self.accuracy: float = None\n        self.max_missed: int = None\n        self.margin: float = None\n        self.accel: float = 0.0\n        self.veloc: float = 0.0\n        self.scv: float = 0\n        \n        self.home_steps: float = None\n        \n        self.tries: int = 0\n        self.move: Move = None\n        self.move_dist: float = 0.0\n        self.move_valid = True\n        self.move_missed: dict = None\n        self.move_time_prehome: float = 0.0\n        self.move_time: float = 0.0\n        self.move_time_posthome: float = 0.0\n        self.time_start: float = 0.0\n        self.time_last: float = 0.0\n        self.time_total: float = 0.0\n\n    def __str__(self):\n        fmt = f\"AttemptWrapper {self.type} on {self.axis}, try {self.tries}\\n\"\n        fmt += f\"| Min: {self.min:.0f}, Max: {self.max:.0f}\\n\"\n        fmt += f\"| Accuracy: {self.accuracy*100}%, Max Missed: {self.max_missed:.0f}\\n\"\n        fmt += f\"| Margin: {self.margin}, Accel: {self.accel:.0f}, Veloc: {self.veloc:.0f}\\n\"\n        fmt += f\"| Move: {self.move}\"\n        fmt += f\"| Valid: {self.move_valid}, Dist: {self.move_dist:.0f}\\n\"\n        fmt += f\"| Times: {self.move_time_prehome:.2f}/{self.move_time:.2f}/{self.move_time_posthome:.2f}s over {self.time_last:.2f}\"\n        return fmt\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/bash\n# automatically calculate your printer's maximum acceleration/velocity\n#\n# Copyright (C) 2024 Anonoei <dev@anonoei.com>\n#\n# This file may be distributed under the terms of the MIT license.\n\n# Force script to exit if an error occurs\nset -e\n\nKLIPPER_PATH=\"${HOME}/klipper\"\nSYSTEMDDIR=\"/etc/systemd/system\"\nSRCDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\"/ && pwd )\"\n\n# Verify we're running as root\nif [ \"$(id -u)\" -eq 0 ]; then\n    echo \"This script must not run as root\"\n    exit -1\nfi\n\n# Check if Klipper is installed\nif [ \"$(sudo systemctl list-units --full -all -t service --no-legend | grep -F \"klipper.service\")\" ]; then\n    echo \"Klipper service found!\"\nelse\n    echo \"Klipper service not found, please install Klipper first\"\n    exit -1\nfi\n\n# Check for old python\n~/klippy-env/bin/python -c 'import sys; assert sys.version_info[0] == 3, \"Python 3 is required.\"'\n\n# Link auto speed to klipper\necho \"Linking auto speed to Klipper...\"\nln -sf \"${SRCDIR}/auto_speed.py\" \"${KLIPPER_PATH}/klippy/extras/auto_speed.py\"\nmkdir -p \"${KLIPPER_PATH}/klippy/extras/autospeed\"\nfor file in `ls autospeed/*.py`; do\n    ln -sf \"${SRCDIR}/${file}\" \"${KLIPPER_PATH}/klippy/extras/${file}\"\ndone\n\n# Install matplotlib\necho \"Installing matplotlib in klippy...\"\n~/klippy-env/bin/python -m pip install matplotlib\n\n# Restart klipper\necho \"Restarting Klipper...\"\nsudo systemctl restart klipper"
  }
]