Analog Sensor Integration MANTA M8P

Hi everyone,

I’m working on integrating a reflectivity sensor (phototransistor-based) with my Manta M8P. My goal is to capture live analog voltage readings (0–3.3 V) from the sensor so I can use it to trigger a roller to spin (which I have configured as an extruder) when a reflective aluminium surface passes under it.

I’ve been trying to leverage Klipper’s analog-capable pins, similar to how thermistor pins are used. Specifically, I was hoping to read the sensor voltage using QUERY_ADC or a similar mechanism.

The problem I’m running into:

  • When I try QUERY_ADC or use the “temperature_sensor” interface, I don’t get the raw voltage; I only get processed values or timeouts.

  • Accessing raw ADC values directly doesn’t seem to be supported out-of-the-box, and the existing temperature sensor framework is oriented around thermistor readings rather than arbitrary analog voltages.

  • Manual tests via Moonraker show null or unexpected values, and polling the ADC from Python doesn’t return actual voltages.

What I’m trying to figure out:

  • What is the best way to capture raw ADC values from an analog pin on an STM32 MCU in Klipper?

  • Is there a recommended approach for creating a custom sensor that behaves like a thermistor but returns raw voltage values from 0–3.3 V?

  • Has anyone successfully repurposed an analog-capable pin for this kind of application?

I’d really appreciate any guidance on how to approach this — whether it involves firmware changes, custom modules in extras/, or another method entirely.

Thanks in advance!

I think we have [adc_temperature] section which uses linear interpolation between points, so by defining points like 0/0 and 3.3/3.3 should give you the reading.
But not sure if it will help if QUERY_ADC doesn’t get you expected results.

You may also need to check for electronics issues. If you just connected one pin of phototransistor to 3.3v and the other to ADC, that probably won’t work due to ADC high input resistance.

1 Like

Do you really need “raw ADC values” or do you actually need ADC values that indicate the light level shining on the phototransistor?

I use the adc_temperature statement with a couple of resistors to monitor the input voltage going into a controller board as detailed here:

This is being used as a basis for the functional test suite I’ve been working on to check the input voltage values and, more importantly, the ADC circuitry on the controller board.

Using a macro, I can test the voltage values (which are derived from the measured ADC voltages in the adc_temperature statement and respond accordingly:

[gcode_macro vintest]   
gcode:
    {% set VINMON_VALUE = ( printer["temperature_sensor PS_voltage"].temperature )|float %}
    RESPOND TYPE=command MSG="VINTest: { VINMON_VALUE }V"
    {% if 23.5 > VINMON_VALUE %}      
        RESPOND TYPE=error MSG="VINTest: Voltage BELOW 24V"
    {% else %}
        {% if 24.5 < VINMON_VALUE %}      
            RESPOND TYPE=error MSG="VINTest: Voltage ABOVE 24V"
        {% else %}
            RESPOND TYPE=command MSG="VINTest: PASS"
        {% endif %}
    {% endif %}

It may take a bit of work to determine the conversion value but it works very well and, in my case, the “temperature” values accurately correspond to the actual voltages input into the board.

2 Likes

Thanks so much for sharing this example. I was trying to use this interpolation method but was getting slightly off ADC values for the sensor from the corresponding temperature recordings. I only require the ADC values to indicate the level of shining correct so I’m hoping I can either get the default adc_temperature function in Klipper working or I was even thinking to create my own adv_voltage.py function in the backend to skip the interpolation of the ADC values to temperature and just directly extract the ADC values from the phototransistor.

In my attempt at this so far, I have managed to get Klipper to recognise a custom object named adc_voltage_sensor blister_sensor, meaning the [adc_voltage my_sensor] section in my Klipper config gets parsed. However, when I got the API output for this object, I could see that Klipper is actually not publishing any value field (unlike [temperature_sensor], which publishes a "temperature" value. I got this: {“result”: {“eventtime”: …, “status”: {“adc_voltage_sensor blister_sensor”: {}}}}

I must have not correctly linked my adc_voltage.py to Klipper’s backend’s “sensor data publication” system yet. I need to find a way to expose the captured ADC values to the Klipper host API by registering an output field (like "voltage").

I could upload the modified code if you want but I was not sure if it falls beyond the scope of this forum. I may have to revert to just using the inbuilt adc_temperature method again and get it accurate this time round.

By all means, post your code here. I’m in no way an expert on it but I’m sure the people who are (and maintain the code bases) will have some useful comments.

So this is the adc_voltage_sensor.py custom code I compiled and have included in klippy/extras, which was inspired by the adc_temperature.py code. I have ensured that Klipper’s ADC sampling system would call my callback properly registering the object with query_adc. Although, I keep seeing a blank output {} in the API. The sensor appears to exist and is registered, but Moonraker has no valid status to report.

I’m also now having an issue where my Klipper board just crashes out when I try and run my polling function in my GUI. What happens is as soon as polling for this adc value starts. the monitor loses signal and the power LED next to the pi goes solid red. Although, I’m still able to carry out the lighter tasks like homing and position changes as normal. I noticed it started happening when I added query_adc in the adc_voltage.py code although the same issue keeps happening now when I revert to my old code so I don’t know if I have damaged something or if Im polling the mcu too quickly. I tried increasing the sample and report time in my adc_voltage_sensor.py code but no improvement.

I’m honestly so stuck now and may have to just go back to basics with what already exists.

#adc_voltage_sensor.py

import logging

SAMPLE_TIME = 0.001SAMPLE_COUNT = 8REPORT_TIME = 0.300RANGE_CHECK_COUNT = 4

class ADCVoltageSensor:
    def init(self, config): 
    self.printer = config.get_printer()
    self.name = config.get_name().split()[-1]
    self.pin = config.get(‘pin’)  

    ppins = self.printer.lookup_object('pins')
    self.mcu_adc = ppins.setup_pin('adc', self.pin)

    # Register with Klipper's internal ADC query handler
    query_adc = self.printer.load_object(config, 'query_adc')
    query_adc.register_adc(self.name, self.mcu_adc)

    # Configure sampling (same as Klipper's thermistor sensors)
    self.mcu_adc.setup_adc_sample(
        SAMPLE_TIME, SAMPLE_COUNT,
        minval=0.0, maxval=1.0,
        range_check_count=RANGE_CHECK_COUNT
    )

    self.last_voltage = 0.0
    self.mcu_adc.setup_adc_callback(REPORT_TIME, self._adc_callback)

    # Register object for Moonraker visibility
    self.printer.add_object(f"adc_voltage_sensor {self.name}", self)
    logging.info(f"[adc_voltage_sensor] '{self.name}' on {self.pin}")

    def _adc_callback(self, read_time, read_value):
    # Convert normalized 0-1 range to actual volts (assuming 3.3 V ref)
        self.last_voltage = read_value * 3.3

    def get_status(self, eventtime):
        return {"voltage": round(self.last_voltage, 3)}


def load_config_prefix(config):
    return ADCVoltageSensor(config)

The sensor is configured in my printer.cfg file as:

[adc_voltage_sensor blister_sensor]

pin: PA7

Hi!

This line isn’t necessary, as Klipper automatically adds the object upon initialization.

If you add this line into _adc_callback(), does anything show up in klippy.log? I would also try setting the poll interval to 0.1 or even 0.5 for testing.

logging.info(f"[adc_voltage_sensor] '{self.name}' on {self.pin} READ = {self.last_voltage:.3f}")
2 Likes

Thanks for the reply. I removed the G-code command that rotates the motor when sensor is triggered for now and the board stopped crashing when I ran the poll. Do you know how I can overcome this? Any tips for this is highly critical to get this working.

I kept the SAMPLE_TIME = 0.001 & REPORT_TIME = 0.300 as default since started getting strange errors like MCU ‘mcu’ shutdown: Rescheduled timer in the past in the logs and Klipper would not reset. This happened when I set reporting time equal to 0.1 and sample time to 0.5, which Klipper’s internal event scheduler appears to not be able keep up with the number or frequency of step pulses it’s being asked to generate in this instance.

Now when I added the logging line into _adc_callback() I do indeed see I am getting ADC values matching what I am measuring on my multimeter although I still get voltage of 0 outputted on my GUI. I realised I had mistakenly put the incorrect object name when call Moonraker’s /printer/objects/query endpoint, I had used voltage_sensor blister_sensor instead of adc_voltage_sensor blister_sensor.

This is the log output showing desired ADC values:

Stats 376.0: gcodein=0 mcu: mcu_awake=0.000 mcu_task_avg=0.000000 mcu_task_stddev=0.000000 bytes_write=3436 bytes_read=7014 bytes_retransmit=9 bytes_invalid=0 send_seq=243 receive_seq=243 retransmit_seq=2 srtt=0.001 rttvar=0.000 rto=0.025 ready_bytes=0 upcoming_bytes=0 freq=519972521 print_time=3.360 buffer_time=0.000 print_stall=0 extruder: target=0 temp=0.0 pwm=0.000 sysload=0.91 cputime=0.258 memavail=6218592
**\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.993
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.992**
Stats 377.0: gcodein=0 mcu: mcu_awake=0.016 mcu_task_avg=0.000014 mcu_task_stddev=0.000058 bytes_write=3442 bytes_read=7148 bytes_retransmit=9 bytes_invalid=0 send_seq=244 receive_seq=244 retransmit_seq=2 srtt=0.001 rttvar=0.000 rto=0.025 ready_bytes=0 upcoming_bytes=0 freq=519986117 print_time=3.360 buffer_time=0.000 print_stall=0 extruder: target=0 temp=-83.4 pwm=0.000 sysload=0.91 cputime=0.261 memavail=6220608
**\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.992
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991**
Stats 378.0: gcodein=0 mcu: mcu_awake=0.016 mcu_task_avg=0.000014 mcu_task_stddev=0.000058 bytes_write=3448 bytes_read=7269 bytes_retransmit=9 bytes_invalid=0 send_seq=245 receive_seq=245 retransmit_seq=2 srtt=0.001 rttvar=0.000 rto=0.025 ready_bytes=0 upcoming_bytes=0 freq=519989922 print_time=3.360 buffer_time=0.000 print_stall=0 extruder: target=0 temp=-88.5 pwm=0.000 sysload=0.91 cputime=0.263 memavail=6249712
**\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.992
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991**
Stats 379.0: gcodein=0 mcu: mcu_awake=0.016 mcu_task_avg=0.000014 mcu_task_stddev=0.000058 bytes_write=3454 bytes_read=7375 bytes_retransmit=9 bytes_invalid=0 send_seq=246 receive_seq=246 retransmit_seq=2 srtt=0.001 rttvar=0.000 rto=0.025 ready_bytes=0 upcoming_bytes=0 freq=519994434 print_time=3.360 buffer_time=0.000 print_stall=0 extruder: target=0 temp=-79.7 pwm=0.000 sysload=0.91 cputime=0.265 memavail=6254432
**\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.991
\[adc_voltage_sensor\] ‘blister_sensor’ on PA7 READ = 2.992**

Can you please post your updated python code?

Klipper is pretty picky about asynchronous stepper motion. I suspect you’ll have to acquire the G-Code mutex, which will complicate your code quite a bit as you need to queue your motions then execute them.

PLEASE: When you upload code, use the Preformatted text feature of the forum editor to make it more readable. Thank you.
Format

1 Like