Introduction: Smart 3D Printer Filament Counter

Why bother counting filament? A few reasons:

  • Successful prints require a properly-calibrated extruder: when the gcode tells the extruder to move the filament 2mm, it needs to move exactly 2mm. Bad things happen if it over-extrudes or under-extrudes. A well-calibrated counter can keep an extruder honest.
  • Slicers approximate how much total filament a given print will take (in both length and weight) and I'd like to check those values.
  • Measuring the movement of filament also let's me know when printing has started and when it has stopped.
  • I needed something to cover the space left by the removal of the ugly giant logo on the front of my printer.
  • It's cool.

I was inspired by this instructable, which repurposed an old PS/2 mouse as a filament counter for a 3D Printer. Not only did it add a useful feature to a 3D printer, it repurposed an old device that would've otherwise ended up in a landfill. But that project was built around the mouse's PS/2 interface, which seemed needlessly cumbersome. So I took this as an opportunity to learn about the only essential component: the rotary encoder.


  • Rotary encoder
  • ESP32 based dev board
  • I2C OLED display (two-color unit looks especially cool)
  • Tiny momentary pushbutton
  • De-greased 608ZZ bearing
  • Two o-rings from the hardware store (~33mm ID x ~1.5mm profile diameter - see comments)
  • Two 2.5mm self-tapping screws for the enclosure
  • Two 4mm screws, nuts, and washers to attach the mount to your printer
  • Bunch of wires
  • 3D Printer and some filament

Step 1: Choose a Rotary Encoder

Rotary encoders translate rotational movement into electrical pulses. All old-school mice used them to measure the movement of the rolling ball, and more-modern (ha ha) optical mice still used them for the scroll wheel, which is what I had laying around and used for initial experimentation. Unfortunately, mine offered no obvious mount points and its resolution was poor.

If it's worth doing, it's worth over-doing. So I bought a large, friendly, 360-pulse per revolution encoder and built my project around it. The one I chose was a Signswise Incremental Optical Rotary Encoder, type LPD3806-360BM-G5-24C. But any decent encoder will do.

Step 2: Add a Pulley and Idler

Linear movement of the filament is translated into rotational movement of the encoder by a pulley. And the filament is held against the pulley by an idler.

The pulley has two grooves, each holding a stretched o-ring so there's no slipping,

The idler has a single v-groove to keep the filament centered on the encoder pulley. It sits on a 608ZZ bearing I had laying around, and that's mounted on a spiral spring printed right in the main body of my project. (STL files attached below.)

This took some trial and error to get right, but my design should accommodate a variety of angles and spool radii, allowing the filament to unwind from any part of the spool, all the way from the beginning to the end of a print. And the printed spring makes it easy to pop in or out the filament when changing spools.

Step 3: Coding

For just counting filament, any dev board with two digital inputs will do. The encoder I chose has four pins: Vcc, ground, and two encoder pins. Here's a really nice write-up that explains how rotary encoders work and how to interface them with Arduino. (Also: this article about 3-pin encoders.)

The basic counting is simple: two inputs -- set to pull up internally so external resistors don't need to be soldered to Vcc -- and one interrupt. I also added a zero/reset button, requiring one more input and interrupt:

void setUpPins() {

  attachInterrupt(ENCODER_PIN_1, encoderPinDidChange, CHANGE);
  attachInterrupt(ZERO_BTN_PIN, zeroButtonPressed, CHANGE);

void IRAM_ATTR encoderPinDidChange() {
  if (digitalRead(ENCODER_PIN_1) == digitalRead(ENCODER_PIN_2)) {
    position += 1;
  } else {
    position -= 1;

void IRAM_ATTR zeroButtonPressed() {
  // handle zero & reset

But I wanted more than just a dumb counter. With an ESP32 (or ESP8266) and its built-in WiFi, I can actually do something with the data I'm collecting. Using some simple timeout code (explained below), I can determine when printing starts and ends, and send those events as notifications to my phone. In the future, I may add a run-out sensor and notify myself (and pause my printer) when my attention is needed.

The full code is on Github.

A few notes on the code:

  • To customize this to your build, all you need is the resolution (encoderPPR) -- in pulses per revolution, which is typically twice the stated spec -- and the radius of the pulley (wheelRadius). These values, plus the ssid and password of your wifi and the specific pins connected to the button, encoder, and OLED screen, all go in config.h.
  • The zero button also doubles as a reset - hold it down to reboot the board, which is useful for debugging.
  • Interrupts are powerful - sometimes too powerful. A single tap of the zero button could cause the zeroButtonPressed() function to be called 10-20 times, so I added some debounce logic. My optical encoder didn't need it, but YMMV.
  • While the interrupts take care of the inputs asynchronously, the loop() routine handles the bookkeeping. The encoderState -- an enum that can be feeding, retracting, or stopped -- is updated with the change in position of the encoder. Timeouts then determine when the printer has commenced and concluded printing. But the tricky part is that 3D printers frequently start and stop movement, so what worked best was to define the "print complete" event remaining continuously stopped for at least 5 seconds. Any motion triggers a second timer that defines the "printing started" event only if no "print complete" event occurs in a timeframe of 15 seconds. In practice, this works swimmingly.
  • So the main loop() code can run unencumbered, the debounce code runs in an RTOS task loop. Likewise, http requests to send off notifications are synchronous and therefore backgrounded. Thus animations run smoothly and the counting never stops.
  • There's a bunch of additional code in my example to (A) establish and maintain a network connection with WiFi and mDNS, (B) fetch the time from an NTC server so I could time-stamp my start and end notifications and display a jaunty clock on my OLED, and (C) handle OTA updates so I don't have to physically connect my board to my Mac for code updates. At the moment, it's all in one monolithic C++ file, only because I haven't taken the time to organize it better.
  • I used the wonderful (and free) Prowl iOS app to send push notifications to my phone with nothing more than HTTP Get methods.
  • For my project, I used these libraries: u8g2 by Oliver, elapsedMillis by Paul Stoffregen, and HTTPClient by Markus Sattler, which comes with the Espressif ESP32 platform. Everything else either comes with the Arduino library or the ESP32 platform in PlatformIO.
  • Finally, I created six simple bitmaps of my main pulley at different angles, so I could show a neat little spinning wheel animation on the OLED behind the counter. It moves in the appropriate direction with the encoder, although much faster for a more dramatic effect.

Step 4: Wiring

I designed this so wiring would be dead simple, mostly so my enclosure could be small, but also so debugging would be straight-foward. Note the cramped conditions in my little box. :)

The first requirement was the 5V supply voltage of my rotary encoder. Of the various ESP32 dev boards I had in my stash, only a few supplied true 5V at the Vcc pin when powered by USB. (The others measured 4.5-4.8V, which, in case your math is bad, is lower than 5V.) The board I used was a Wemos Lolin32.

Next, come the two rotary encoder signal pins. Since I'm using interrupts, the main concern is that the pins I use don't interfere with anything. The ESP32 docs state that ADC2 can't be used at the same time as WiFi, so that unfortunately means I can't use any of the ADC2 GPIO pins: 0, 2, 4, 12, 13, 14, 15, 25, 26, or 27. I chose 16 and 17.

Pro tip: if, after putting all this together, your encoder seems to be counting backwards, you can just swap the two pin assignments in config.h.

Finally, connect the rotary encoder ground wire to... drum roll... the ground pin.

Next, the zero/reset push button gets connected between ground and another free pin (I chose GPIO 18).

The button I used was a tiny momentary switch I rescued from the aforementioned computer mouse, but any button you have laying around will do. You can see it resting in a little mount I made for it right over the board.

Finally, the OLED, if it's not already connected to your board, needs just four pins: 3V3, ground, i2c clock, and i2c data. On my dev board, clock and data are 22 and 21, respectively.

Step 5: Print Out the Parts

I designed seven parts for this build:

  • The pulley, which mounts directly on the shaft of the rotary encoder.
  • The idler, which fits over a 608ZZ bearing (remove the sheilds and degrease with WD40 so it spins freely).
  • The holder, on which the two weels and encoder mount - note the spiral spring for the idler.
  • A bracket to stabilize the holder. The photo in this step shows how the bracket attaches to the holder.
  • The enclosure (bottom) to hold my ESP32 dev board, with a space for the USB cable on the side and another on top for the connector I added to my encoder wires. This one is designed to fit the Wemos Lolin32, so you might have to mod this design a little to fit a different board.
  • The enclosure (top) to hold the OLED screen, another spiral for the zero / reset button
  • A button holder customized for the tiny switch I had, designed to rest between the two shelves inside the bottom enclosure. I used a soldering iron to "glue" the switch to the holder; see the prior step for a photo.

Everything is designed to be printed without supports. Normal PLA in your color of choice is all you need.

Put it all together, attach to your printer (some creativity may be required here), and you're good to go.

3D Printed Contest

Participated in the
3D Printed Contest