Skip to content

Instantly share code, notes, and snippets.

@kmobs
Last active May 4, 2025 17:47
Show Gist options
  • Save kmobs/f6def5db272ca5c1b81727482f53bed8 to your computer and use it in GitHub Desktop.
Save kmobs/f6def5db272ca5c1b81727482f53bed8 to your computer and use it in GitHub Desktop.
Frequency Tester

Frequency Testing

This will allow you to test a specific frequency that you hear when you do your resonance testing in Klipper and potentially track down where extra peaks are coming from. If something is rattling at a specific frequency, you can specify that frequency and feel around the printer until you track it down.

Code was written by Zifnab.

Usage

You have to have [resonance_holder] in your printer.cfg. The command is HOLD_RESONANCE AXIS=<axis> FREQ=int SECONDS=<seconds>

Place the following in extras/resonance_holder.py and restart klipper (not firmware restart) to gain the module.


class TestAxis:
    def __init__(self, axis=None, vib_dir=None):
        if axis is None:
            self._name = "axis=%.3f,%.3f" % (vib_dir[0], vib_dir[1])
        else:
            self._name = axis
        if vib_dir is None:
            self._vib_dir = (1., 0.) if axis == 'x' else (0., 1.)
        else:
            s = math.sqrt(sum([d*d for d in vib_dir]))
            self._vib_dir = [d / s for d in vib_dir]
    def matches(self, chip_axis):
        if self._vib_dir[0] and 'x' in chip_axis:
            return True
        if self._vib_dir[1] and 'y' in chip_axis:
            return True
        return False
    def get_name(self):
        return self._name
    def get_point(self, l):
        return (self._vib_dir[0] * l, self._vib_dir[1] * l)

def _parse_axis(gcmd, raw_axis):
    if raw_axis is None:
        return None
    raw_axis = raw_axis.lower()
    if raw_axis in ['x', 'y']:
        return TestAxis(axis=raw_axis)
    dirs = raw_axis.split(',')
    if len(dirs) != 2:
        raise gcmd.error("Invalid format of axis '%s'" % (raw_axis,))
    try:
        dir_x = float(dirs[0].strip())
        dir_y = float(dirs[1].strip())
    except:
        raise gcmd.error(
                "Unable to parse axis direction '%s'" % (raw_axis,))
    return TestAxis(vib_dir=(dir_x, dir_y))


class ResonanceHolder:
    def __init__(self, config):
        self.printer = config.get_printer()
        self.gcode = self.printer.lookup_object('gcode')
        self.accel_per_hz = config.getfloat('accel_per_hz', 75., above=0.)
        self.gcode.register_command("HOLD_RESONANCE", self.cmd_HOLD_RESONANCE, desc=self.cmd_HOLD_RESONANCE_help)

    def hold(self, gcmd, axis, seconds, freq):
        '''holds a resonance for N seconds
           resonance code taken from klipper's test_resonances command'''
        end = time.time() + seconds
        toolhead = self.printer.lookup_object('toolhead')
        X, Y, Z, E = toolhead.get_position()
        sign = 1.
        input_shaper = self.printer.lookup_object('input_shaper', None)
        if input_shaper is not None and not gcmd.get_int('INPUT_SHAPING', 0):
            input_shaper.disable_shaping()
            gcmd.respond_info("Disabled [input_shaper] for resonance holding")
        else:
            input_shaper = None
        gcmd.respond_info("starting freq %i for %i seconds" % (freq, seconds))
        while time.time() < end:
            t_seg = .25 / freq
            accel = self.accel_per_hz * freq
            max_v = accel * t_seg
            toolhead.cmd_M204(self.gcode.create_gcode_command(
                "M204", "M204", {"S": accel}))
            L = .5 * accel * t_seg**2
            dX, dY = axis.get_point(L)
            nX = X + sign * dX
            nY = Y + sign * dY
            toolhead.move([nX, nY, Z, E], max_v)
            toolhead.move([X, Y, Z, E], max_v)
            sign = -sign
        gcmd.respond_info("DONE")


    cmd_HOLD_RESONANCE_help = ("Holds resonance for n seconds")
    def cmd_HOLD_RESONANCE(self, gcmd):
        axis = _parse_axis(gcmd, gcmd.get("AXIS").lower())
        freq = gcmd.get("FREQ", parser=int)
        seconds = gcmd.get("SECONDS", parser=int)
        self.hold(gcmd, axis, seconds, freq)

def load_config(config):
    return ResonanceHolder(config)```
@simonvez
Copy link

Awesome guys !! Thanks so much. Funny I was discussing with my team last week about this and how it would be useful to have this. You rock!! Thats going to be useful!

@jeremytodd1
Copy link

jeremytodd1 commented Jun 19, 2023

I'm trying to get this working but I keep running into a "Section 'resonance_holder' is not a valid config section" error message.

resonance2
error

inside printer.cfg:
klippy2

Few picture for reference.

Any ideas? This is on a Voron 2.4 running Klipper/Mainsail.

@kmobs
Copy link
Author

kmobs commented Jun 19, 2023

Restart klipper completely with systemctl or reboot your pi since you're adding a new extra .py file.

@jeremytodd1
Copy link

I ended up figuring it out. I did try both a restart and a full power down/turn on with the same error coming up after.

To make the folder structure for the resonance_holder.py file, I just added the files through the Mainsail GUI. I went to the Machine tab -> Add folder -> add folder -> create the resonance_holder.py file and then paste the code in it. I then rebooted and got the error.

Apparently, when you create the folders/file that way, the path ends up being:
/home/pi/printer_data/config/klippy/extras/resonance_holder.py

I then found an already created "klippy" folder at:
/home/pi/klipper/klippy

So I followed the process for that location instead and it worked.

In my defense, the guide says to "Place the following in klippy/extras/resonance_holder.py".
I figured just making the folders via the GUI would suffice but apparently not lol
It never specified they need to be in the "klipper/klippy/extras" section

Glad I got it working though. This tool will be very useful. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment