Read ACCELEROMETER_QUERY response in macro

Basic Information:

Printer Model: Functional Tester
MCU / Printerboard: KGP 4x2209
Host / SBC: CM4
klippy.log: N/A

In my functional test code, I’m checking for the presence of an ADXL345 using a macro to execute the ACCELEROMETER_QUERY command but I can’t figure out how to read and parse the response in the macro.

I’ve also looked at trying to change the ADXL234 values dynamically (which I should be able to capture the response) but I haven’t found any printer[adxl345]. objects that I can change (I was hoping for axes_map).

The ADXL345 is defined in printer.cfg as:

[adxl345]
spi_bus: spi1
cs_pin: PB0
axes_map: x,z,y

In my searching on the topic, I did find:

But, this is five years ago and I don’t think the response is appropriate for the very basic set up that I’m running for the functional test.

I’m looking at klipper_repl:

and I think it might work but when I try to install it using pip:

I’m sure I’m missing something basic here but I just can’t see it.

Thanx for any help and suggestions.

One comment to help explain what I’m doing and what I need. Looking back over the original post, I could have explained things better.

A script runs the main test code (which involves Flashing the Board Under Test’s MCU as well as accessing some simple python modules to exercise hardware attached to the board under test). For accessing hardware I have been executing the macros which execute the G-Code Commands in the Console and then parse the response, like:

[gcode_macro testmacro09]   
gcode:
    {% set THERM0_VALUE = ( printer["heater_generic heater0"].temperature )|float %}
    RESPOND TYPE=command MSG="Test27: HEATER0 Temperature Check Test: { THERM0_VALUE }C"
    {% if 30 > THERM0_VALUE %}      
        RESPOND TYPE=error MSG="Test27: HEATER0 Too Cool"
    {% else %}
        {% if 50 < THERM0_VALUE %}      
            RESPOND TYPE=error MSG="Test27: HEATER0 Too Warm"
        {% else %}
            RESPOND TYPE=command MSG="Test27: HEATER0 Test: PASS"
        {% endif %}
    {% endif %}

Now, I don’t have to execute a macro, it’s just that this is how I’ve been doing things so far, which is why I’m looking at klipper-repl.

If I run ACCELEROMETER_QUERY in a console with an ADXL345 installed and accessible, the information returned is:

if there isn’t an ADXL345 present, I expect to get:

Again, any suggestions on how to would be appreciated.

try replacing sudo pip install ... with ~/klippy-env/bin/pip install ...

Hi @mykepredko ,

After looking at adxl345.py, it looks like there isn’t an official status reference to read values from. However, I was able to dissect the ACCELEROMETER_QUERY command to make a simple Python script that can be run via Dynamic Macros to read the accelerometer values. You can probably do something similar to add a proper status reference if you wanted (via a get_status() function), which could be read by the Moonraker API.

# macros.cfg
[gcode_macro READ_ADXL]
gcode:
  {% set values = python_file('read_adxl.py') %}
  RESPOND MSG="{values.x}, {values.y}, {values.z}"
# read_adxl.py
adxl = printer.lookup_object('adxl345')
aclient = adxl.start_internal_client()
printer.lookup_object('toolhead').dwell(.1)
aclient.finish_measurements()
values = aclient.get_samples()
_, x, y, z = values[-1]
output({
  "x": x,
  "y": y,
  "z": z,
})

Hopefully that helps.

@bozzo

That did the trick, klipper-repl is now installed in ~/klippy-env/bin:

Now, I don’t have a link to the Klipper API (or Klipper itself) in the run folder:

This is important because when I try to run klipper-repl with /run/klipper/api as suggested in the klipper-repl GitHub page, I get:

Reading through the klipper-repl Discourse announcement:

I tried using printer_data/comms/klippy.sock (which is used there but got the same error as the last post in the thread):

Now, I did try restarting the Klipper API (I know I shouldn’t have bothered, but I had to try) using the command:

~/klippy-env/bin/python ~/klipper/klippy/klippy.py ~/printer_data/config/printer.cfg -a /tmp/klippy_uds -l /tmp/klippy.log`

This is from the Klipper API page:

I changed the printer.cfg location to match what was in the system.

I’m guessing I’m not providing the correct link to the Klipper API Socket, but I have no idea where it is.

Any ideas for next steps?

Thanx.

@3dcoded

Thanx for the suggestion and example code.

I created the readadxl.py file (I changed it from “read_adxl” to avoid any underscore issues) what what should be the correct format for the Dynamic Macro Python file as described in

class READADXL:
    def __init__(self, config):
        self.printer = config.get_printer()
        self.reactor = self.printer.get_reactor()
        self.gcode = self.printer.lookup_object('gcode')

        self.message = config.get('message', 'Read ADLX345 X/Y/Z Values')

        self.gcode.register_command(
            'READADXL', self.cmd_READADXL)
        self.printer.register_event_handler(
            'klippy:ready', self._ready_handler)

    def _ready_handler(self):
        waketime = self.reactor.monotonic() + 1
        self.reactor.register_timer(self._readadxl, waketime)

    def _readadxl(self, eventtime=None):
        self.gcode.respond_info(self.message)
        adxl = printer.lookup_object('adxl345')
        aclient = adxl.start_internal_client()
        printer.lookup_object('toolhead').dwell(.1)
        aclient.finish_measurements()
        values = aclient.get_samples()
        _, x, y, z = values[-1]
        output({
         "x": x,
         "y": y,
         "z": z,
        })
        return self.reactor.NEVER

    def cmd_READADXL(self, gcmd):
        self._readadxl()

def load_config(config):
    return readadxl(config)

and stored it in ~/klipper/klippy/extras as noted in the instructions.

I used the macro you suggested as is with the changed name:

[gcode_macro READADXL]
gcode:
  {% set values = python_file('readadxl.py') %}
  RESPOND MSG="{values.x}, {values.y}, {values.z}"

I did a Klipper Restart and was pleasantly surprised to see that the host version of Klipper is now “dirty” (expected due to adding the readadxl.py file):

Tried running the macro and

Any suggestions on what I’ve done wrong? I think I have correctly set up the different READADXL class members and put the python file in the correct location.

Thanx!

netstat --unix -p or cat /proc/net/unix might give some clues where the active klippy socket is located.

I have not used dynamicmacros, from the errors you get it would seem you did not actually install dynamicmacros extension and instead created a standalone klipper extension that registers command READADXL and then proceeded to override the registered command with one that assumes existence of dynamicmacros.

I’m quite sure the contetns of the python file suggested by @3dcoded are meant verbatim, and I strongly suspect they are not meant to go into the klippy/extras folder. To make it work, you’d need to go through the setup procedure for dynamicmacros as described on the dynamicmacros site. Test the install with the tutorial from there first and then read up where exactly you should put the read_adxl.py my bet is next to the macro file.

1 Like

First, a few explanations about Dynamic Macros. Dynamic Macros is an extra I designed to allow for updating macros without restarting Klipper, but it has since expanded to allow a host of features, Python code included. Dynamic Macros’s features can only be used by installing it (yes this will make Klipper “dirty”) and configuring as explained in its docs. The code I provided will work verbatim with Dynamic Macros. I tested it on my setup.

This problem can be approached in two main ways: DynamicMacros or extras.


DynamicMacros Approach

It looks like you created it as a full extra. The code I provided will work verbatim. Just put read_adxl.py in your printer config folder.

Like @bozzo said, this indicates that Dynamic Macros wasn’t installed or configured properly. Dynamic macros store macros in a separate file then read them with a [dynamicmacros] section, as explained in the docs.


Extras

If you want to do an extra that can provide a status reference for macros or Moonraker, you’re pretty close.

I made a few changes to get it to work as an extras module on my printer (explained in comments)

class READADXL:
    def __init__(self, config):
        self.printer = config.get_printer()
        self.reactor = self.printer.get_reactor()
        self.gcode = self.printer.lookup_object('gcode')

        # Removed self.message

        self.gcode.register_command(
            'READADXL', self.cmd_READADXL)
        self.printer.register_event_handler(
            'klippy:ready', self._ready_handler)
        
        # Added adxl and last values
        self.adxl = self.printer.lookup_object('adxl345')
        self._last_values = {
            "x": 0.0,
            "y": 0.0,
            "z": 0.0,
        }
        self._last_error = ""

    def _ready_handler(self):
        waketime = self.reactor.monotonic() + 1
        self.reactor.register_timer(self._readadxl, waketime)

    # Added get_status()
    def get_status(self, eventtime=None):
        return {
            "last_values": self._last_values,
            "last_error": self._last_error,
        }

    def _readadxl(self, eventtime=None):
        # Added error handling
        try:
            aclient = self.adxl.start_internal_client()
            self.printer.lookup_object('toolhead').dwell(.1)
            aclient.finish_measurements()
            values = aclient.get_samples()
            _, x, y, z = values[-1]
        except Exception as e:
            self._last_error = str(e)
            return self.reactor.NEVER
        # output() only exists in python code executed by Dynamic Macros, not extras.
        self._last_values = {
            "x": x,
            "y": y,
            "z": z,
        }
        return self.reactor.NEVER

    def cmd_READADXL(self, gcmd):
        self._readadxl()

def load_config(config):
    return READADXL(config)

Then, put [readadxl] in your printer.cfg below your [adxl345] configuration.

You can use READADXL in the Mainsail/Fluidd console to update the accelerometer values. To read them, you can either access it via a macro (normal or Dynamic):

[gcode_macro DISPLAY_ADXL]
gcode:
  {% set values = printer.readadxl.last_values %}
  RESPOND MSG="{values.x}, {values.y}, {values.z}"

Output:

> READADXL
> DISPLAY_ADXL
// 536.266849, 148.041188, 10140.821405

Or, you can read it from the Moonraker API. If you go to http://printer.local:7125/printer/objects/query?readadxl, you will get a JSON like this:

{
  "result": {
    "eventtime": 149585.003038914,
    "status": {
      "readadxl": {
        "last_values": {
          "x": 536.266849,
          "y": 148.041188,
          "z": 10140.821405
        },
        "last_error": ""
      }
    }
  }
}

Hopefully that explains things a bit better.

1 Like

Only netstat --unix -p returned a value related to klipper:

I’m not sure what to do with 439/klipper_mcu - could you explain the next steps?

Thanx.

Thank you for the comprehensive answer.

Sorry, I should have provided a more complete reply.

First off, I created a file containing what you provided placed it in ~/klipper/klippy/extras and it wasn’t recognized by Klipper - again, I was expecting to see the host Klipper version become “dirty” which, to me, indicates that it’s recognized by the system.

This never happened - after doing a RESTART, sudo service klipper stop followed by sudo service klipper stop and finally a full power cycle.

So, not seeing “dirty” indicated to me that the file wasn’t recognized.

My response to that was to read through the page and what you presented originally seemed to be the basis for an “extra” - it did not match what was described as a “Dynamic Macro”.

  1. Could you explain how to install what you originally presented?

I tried your updates and no change - same error as above.

Here is the updated readadxl.py:

# My original Code
#class READADXL:
#    def __init__(self, config):
#        self.printer = config.get_printer()
#        self.reactor = self.printer.get_reactor()
#        self.gcode = self.printer.lookup_object('gcode')
#
#        self.message = config.get('message', 'Read ADLX345 X/Y/Z Values')
#
#        self.gcode.register_command(
#            'READADXL', self.cmd_READADXL)
#        self.printer.register_event_handler(
#            'klippy:ready', self._ready_handler)
#
#    def _ready_handler(self):
#        waketime = self.reactor.monotonic() + 1
#        self.reactor.register_timer(self._readadxl, waketime)
#
#    def _readadxl(self, eventtime=None):
#        self.gcode.respond_info(self.message)
#        adxl = printer.lookup_object('adxl345')
#        aclient = adxl.start_internal_client()
#        printer.lookup_object('toolhead').dwell(.1)
#        aclient.finish_measurements()
#        values = aclient.get_samples()
#        _, x, y, z = values[-1]
#        output({
#         "x": x,
#         "y": y,
#         "z": z,
#        })
#        return self.reactor.NEVER
#
#    def cmd_READADXL(self, gcmd):
#        self._readadxl()
#
#def load_config(config):
#    return readadxl(config)

class READADXL:
    def __init__(self, config):
        self.printer = config.get_printer()
        self.reactor = self.printer.get_reactor()
        self.gcode = self.printer.lookup_object('gcode')

        # Removed self.message

        self.gcode.register_command(
            'READADXL', self.cmd_READADXL)
        self.printer.register_event_handler(
            'klippy:ready', self._ready_handler)
        
        # Added adxl and last values
        self.adxl = self.printer.lookup_object('adxl345')
        self._last_values = {
            "x": 0.0,
            "y": 0.0,
            "z": 0.0,
        }
        self._last_error = ""

    def _ready_handler(self):
        waketime = self.reactor.monotonic() + 1
        self.reactor.register_timer(self._readadxl, waketime)

    # Added get_status()
    def get_status(self, eventtime=None):
        return {
            "last_values": self._last_values,
            "last_error": self._last_error,
        }

    def _readadxl(self, eventtime=None):
        # Added error handling
        try:
            aclient = self.adxl.start_internal_client()
            self.printer.lookup_object('toolhead').dwell(.1)
            aclient.finish_measurements()
            values = aclient.get_samples()
            _, x, y, z = values[-1]
        except Exception as e:
            self._last_error = str(e)
            return self.reactor.NEVER
        # output() only exists in python code executed by Dynamic Macros, not extras.
        self._last_values = {
            "x": x,
            "y": y,
            "z": z,
        }
        return self.reactor.NEVER

    def cmd_READADXL(self, gcmd):
        self._readadxl()

def load_config(config):
    return READADXL(config)

For everything above, I hadn’t run the DynamicMacros install instructions in:

simply because it looked like “Extras” were different from “Dynamic Macros” (ie I didn’t see any python code for Dynamic Macros in the examples).

I’ve gone through the instructions for Dynamic Macros and when I try to execute the “readadxl” macro, the same error:

Here is the latest klippy.log (which I should have provided before) along with moonraker.log:

klippy.log (1.9 MB)
moonraker.log (5.3 KB)

  1. Am I missing something in my system configuration?

Thanx for your help

Dynamic Macros can execute Python code. See here

The below is unrelated to the readadxl.py extra.

Sure. To install DynamicMacros, run the following in SSH:

cd ~
git clone https://github.com/3DCoded/DynamicMacros
cd DynamicMacros
sh install.sh
sudo service klipper restart

Then, put the following in moonraker.conf (update manager):

# DynamicMacros Update Manager
[update_manager DynamicMacros]
type: git_repo
path: ~/DynamicMacros
origin: https://github.com/3DCoded/DynamicMacros.git
primary_branch: main
is_system_service: False
install_script: install.sh

To configure it, create a new file in the same folder as your printer.cfg, called dmacros.cfg and put the following in it:

[gcode_macro READ_ADXL]
gcode:
  {% set values = python_file('read_adxl.py') %}
  RESPOND MSG="{values.x}, {values.y}, {values.z}"

Finally, create a file called read_adxl.py in the same folder as printer.cfg and put in it:

adxl = printer.lookup_object('adxl345')
aclient = adxl.start_internal_client()
printer.lookup_object('toolhead').dwell(.1)
aclient.finish_measurements()
values = aclient.get_samples()
_, x, y, z = values[-1]
output({
  "x": x,
  "y": y,
  "z": z,
})

Then, restart Klipper again and run in the console:

READ_ADXL

It should output something like this:

536.266849, 148.041188, 10140.821405

You’re gonna hate me - no joy.

Let’s go through things.

The problem I had here was that you don’t explain that the Python file is in the same folder as printer config (ie ~/printer_data/config). This is why I tried ~/klipper/klipper/extras as that seemed to make the most sense.

Can I suggest that be added to the documentation? I couldn’t find that instruction anywhere there.


I did that as I stated in my previous post.

DynamicMacros installed:

Here is my moonraker.conf:

[server]
host: 0.0.0.0
port: 7125
klippy_uds_address: /home/biqu/printer_data/comms/klippy.sock

[authorization]
trusted_clients:
    192.168.0.0/16
cors_domains:
    *.lan
    *.local
    *://localhost
    *://localhost:*
    *://my.mainsail.xyz
    *://app.fluidd.xyz
    
[octoprint_compat]

[history]

[update_manager]
channel: dev
refresh_interval: 168

[update_manager mainsail]
path: /home/biqu/mainsail
repo: mainsail-crew/mainsail
channel: stable
type: web

[update_manager mainsail-config]
managed_services: klipper
origin: https://github.com/mainsail-crew/mainsail-config.git
path: /home/biqu/mainsail-config
primary_branch: master
type: git_repo

# DynamicMacros Update Manager
[update_manager DynamicMacros]
type: git_repo
path: ~/DynamicMacros
origin: https://github.com/3DCoded/DynamicMacros.git
primary_branch: main
is_system_service: False
install_script: install.sh

read_adxl.py is in ~/printer_data/config:

I added the macro into printer.cfg after the ADXL345 statements:

Did a restart, clicked on “READ ADXL” in the Mainsail console with the result:

Here is the latest klippy.log:

klippy (6).log (503.8 KB)

Sorry for being so much trouble. I’m sure I’m doing/have done something wrong but I can’t figure out what it is.

Here is the process I use for setting up the system/SD Card image for the test process:

I don’t think I’m doing anything unusual or putting the Raspberry Pi CM4 host in some kind of unknown state. It’s a basic minimum 64bit OS, first does the Esoterical CANBus set up, load Klipper using KIAUH, load in Katapult, Load Numpy (obviously for what we’re doing here), Install a Klipper instance on the CM4. @bozzo - you may be interested in this.

Let me know what you want me to try.

Good idea and thank you for the feedback.

For Dynamic Macros, macros are defined in a separate file, not printer.cfg or included by it. For example, if you put the macro in dmacros.cfg, you add the following to printer.cfg:

[dynamicmacros]
configs: dmacros.cfg

As for putting it after adxl345, that’s only necessary if you’re using a custom extra and not Dynamic Macros.

Sorry again for the confusion.

I think we’re confusing each other mixing up dynamic macros and extras. These are two different approaches to solve the same problem, reading accelerometer values into a macro.

Dynamic Macros are defined in a separate file and not included in your printer.cfg. They are instead read by a [dynamicmacros] section in your printer.cfg. The python file, read_adxl.py, is placed in the same folder as printer.cfg. This macro is called READ_ADXL

The extras approach for this is putting a different python file, readadxl.py, is in the extras directory and putting a [readadxl] section in printer.cfg. This approach provides the status reference and Moonraker API access to accelerometer values. This has a command called READADXL with an accompanying macro DISPLAY_ADXL to display the latest accelerometer values.

@mykepredko
Regarding klipper-repl.

I read the post from unjordy, when he posted it Announcing klipper-repl: the missing Klipper command line and was very pleased. But, I never tried it.

Since the sources are quite old (latest from Jun 12, 2023) and Klipper, Moonraker and Fluidd moved on, I imagine there could be problems with klipper-repl and an actual Klipper release.

When you look at the issues GitHub · Where software is built, flowerysong seems to use it.

You could write to unjordy and flowerysong asking for help here in discourse. flowerysong should be definitely reachable here at discourse (his last post is from May 19, 2025).

1 Like

I used it once, yeah.

-a is the flag that tells Klipper where to create the API socket. If you’re running Klipper like this, you would then run klipper-repl /tmp/klippy_uds.

Well, I know that now :slight_smile:

This would be a good introduction to your documentation page:

Looking back at how I did things, I started looking at the first page and, scanning through the table of contents, I jumped down to the “Examples” thinking that they were part of Dynamic Macros. “Extras” seemed to me to be “extra” information, not a different type of Dynamic Macro.

Sorry I had so much trouble figuring out the differences between the two - I’ll try to be less dense going forwards.

I figured this out on my own (by careful reading of the documentation - thanx) and changed Dynamic Macros to include the Dynamic Klipper macro “read_adxl” which checks the results of read_adxl.py:

[gcode_macro READ_ADXL]
gcode:
  {% set values = python_file('read_adxl.py') %}
  RESPOND TYPE=command MSG="\"values\"={values}"
  RESPOND TYPE=command MSG="READ_ADXL: x={values.x}, y={values.y}, z={values.z}"

With this and your read_adxl.py file, things started moving in the right direction and I got some results.

Now, when the ADXL345 is disconnected from the Board Under Test, the “values” returned was printed out as None which I thought was a Python value for a null object.

I changed the read_adxl.py code to:

adxl = printer.lookup_object('adxl345')
aclient = adxl.start_internal_client()
printer.lookup_object('toolhead').dwell(.1)
aclient.finish_measurements()
values = aclient.get_samples()
if values is None:
  output("No ADXL345")
else:
  _, x, y, z = values[-1]
  output({
    "x": x,
    "y": y,
    "z": z,
  })

with the expectation that I would catch the None object but, following the output from my modified Dynamic Macro:

# Correct one?
[gcode_macro READ_ADXL]
gcode:
  {% set values = python_file('read_adxl.py') %}
  RESPOND TYPE=command MSG="\"values\"={values}"
  {% if values == "Python Error" %}
    RESPOND TYPE=command MSG="READ_ADXL: No ADXL345 Present"
  {% else %}
    RESPOND TYPE=command MSG="READ_ADXL: ADXL345 Present"
    RESPOND TYPE=command MSG="READ_ADXL: x={values.x}, y={values.y}, z={values.z}"
  {% endif %}

I see:

When I run the macro from Mainsail, I see the same thing:

Why isn’t Python statement if values is None: acting as if the condition is true?

I believe what I pulled out (in my desperation) was a statement that starts Klipper without any front end and produces a socket to connect to.

If I try klipper-repl with /tmp/klippy_uds I get:

but, if I look at the /tmp directory, I see that there’s klipper_host_mcu which, when run with klipper-repl, I get:

Which never returns.

I’m posting this in the hope this tweaks on somebody.

@hcet14 I’m surprised that you would consider a two year old package as “quite old” - it says a lot regarding how much has been done to Klipper over what I would consider a little while.