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.
Supplies
- 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() { pinMode(ENCODER_PIN_1, INPUT_PULLUP); pinMode(ENCODER_PIN_2, INPUT_PULLUP); pinMode(ZERO_BTN_PIN, INPUT_PULLUP); 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.
- To develop the code and flash the board, I used the spectacular PlatformIO running on Visual Studio Code, both free.
- 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.

Participated in the
3D Printed Contest
51 Comments
11 months ago
is the OLED a .96 inch size or what size?
Question 1 year ago on Step 5
Hello,
For the black OV wire, where does it connect?
1 year ago
Hello, great author, my company has 2 printers, I want to make this counter, I have 3 points to ask: 1. Can you release the code, can it be used in esp8266 (I have esp8266 development board)? / 2. Is there an Android app (I only have an Android phone)? / 3. Do you release ios apps that cannot be used on Android phones? . thank you
3 years ago
That's exactly the solution that I've been looking for. Thank you! Very elegant design 👌
Question 3 years ago
I have the same problem as JosephC277. I took your advice and used PlatformIO to log a 54 minute print of a small part. During the print I logged 10 Stop/Starts and 1 complete reboot (log attached). Currently, minStoppedSession = 5 and minMovementDetected = 15. Do I need to INCREASE these parameters, maybe 1 or 2 seconds and do a reprint? Any other suggestions? Do you or anyone else have any insight on how accurate Ultimaker Cura v6.5 is with its filament use number? Thanks again for you guidance and help!
Answer 3 years ago
Hi L.D.T.,
The timeouts in the project came about after experimenting with my own printer.
The
minStoppedSession value is the number of seconds the counter waits
before considering the print complete. If there's any movement of the
extruder during that time, the timer resets. So if your printer ever
stops extruding from more than 5 seconds, then you should increase this
timeout. This never happens for me, but YMMV.
As for the reboot, your log indicates "Zero button released after long press" immediately beforehand, so the reboot is by design. If you didn't press and hold the button, check for a short or a bad switch. You can also comment out line 356 if this keeps happening or if you don't like that feature :).
I'd love to hear how this goes!
3 years ago
Bonjour,
j'ai voulu réaliser le projet
je me rencontre 2 problème dont 1 que j ai resolu
le premier qui est résolut le fichier config.h est nomme de base config.template.
Mon second souci ,
ou renseigne ton l adresse I2C de l'ecran que l'on install dans le code?
Reply 3 years ago
#define OLED_ADDRESS 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) { return; }
}
If you use the adafruit library, you'd have to make some other (minor) changes to the project. Hope this helps!
Reply 3 years ago
Oui effectivement une simple erreur de ma part
j'ai compile le code tout vas bien par contre je pence pas avoir commande le bon écran oled car l'affichage n est pas centre.
Auriez vous un lien pour commande l’écran oled?
Reply 3 years ago
Any 0.96" I2C 128*64 OLED Screen will do. If you're in France, you can get one here:
https://www.ebay.fr/sch/i.html?_from=R40&_nkw=128+64+oled+i2c&_sacat=0&LH_TitleDesc=0&_sop=12&rt=nc&LH_PrefLoc=1
3 years ago
Hi,
The project is cool and after uploading, it works fine but I have a problem. When I start a print, the counting works very good but several times, the counter resets to zero. Then the counting is false. Can you tell me what it's wrong? Problem of power supply? ( 5V 2 A for me ).
Thanks,
Joseph
Reply 3 years ago
The counter is reset twice in the code. Once, when the zero button is pressed:
and once in the confirmStarted() function when counting has begun:
My guess is that the latter is what's happening to you. To find out for sure, add some logging and watch the terminal as you're printing. You'll probably get some other clues as well. For instance, if confirmStarted() is being hit several times during a print, then perhaps you need to adjust the timeouts for your printer.
It's also possible that a conflict with the interrupts is causing your board to reset. The code saves the current position in eeprom, but doesn't yet save the printing state - so if it resets during a print, the count will be restored, but then zero'd shortly thereafter when it detects that printing has started.
Reply 3 years ago
Ok, thank you for the explanation but I looked in your code and i Don't see where I can adjust the timeouts. I've probably looked the wrong way.
Reply 3 years ago
The two timeouts, minStoppedSession and minMovementDetected, are defined in config.h.
But before you play with those, you really should hook up your unit to the terminal in PlatformIO so you can see what's going on during a print.
Reply 3 years ago
Ok, thanks. Then ,I have to connect the unit to my PC during the print. It's right?
Reply 3 years ago
Yes. If you haven't done this before, I'd suggest you watch a tutorial like this:
You'll want to watch the monitor (terminal) for Serial output so you can see what's going on.
Reply 3 years ago
Ok, very interesting. Thank you, I will check this and come back to communicate the result.
Reply 3 years ago
Hi,
Sorry to disturbing but did you received my question?
Thanks
Question 3 years ago
Still pursuing your project. The #39 O-rings I purchased have an O.D. of 4 mm. I think they are too "fat" for your pulley design. What O.D. do you recommend? Thanks.
Answer 3 years ago
Funny - sorry if the O-ring designation was misleading. I used Danco #39 O-rings, stock #35753B: 1-7/16" OD x 1-5/16" ID. Stupid imperial units. But that makes the diameter of the profile 1/16", or about 1.5mm.
But keep in mind that the o-rings are stretched over the pulley, so anything roughly that size will do. If you can find 1.5mm-thick, 33mm-ID rings, it should work just fine.
I'll update the supplies list accordingly.