C Forde
Published © CC BY-NC-SA

Filament Scale

Rather than a stand alone scale this project integrates the scale into a custom 3D printed spool arm replacing the original in a 3D printer.

IntermediateFull instructions provided8 hours65
Filament Scale

Things used in this project

Hardware components

HX711 and Load Cell
×1
RP2040 with integrated LCD
×1
USBC breakout
×1
100n SMD 1206
×2
Pin Headers Straight
×1
Pin headers right angle
×2
SIL sockets 20 pin
×2
SIL sockets 4 pin
×2
Push Button Switch Momentary SPDT
×3
Toggle switch ON-ON SPDT
×2

Software apps and online services

Thonny
Cura

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free
Wire Cutter / Stripper, 5.25 " Overall Length
Wire Cutter / Stripper, 5.25 " Overall Length

Story

Read more

Custom parts and enclosures

Case

3D print file

Sketchfab still processing.

Bracket

3D print file

Sketchfab still processing.

Nut

3D print file

Sketchfab still processing.

Clamp

3D print file

Sketchfab still processing.

Load

3D print file

Sketchfab still processing.

Schematics

Filament Scale Schematic

Wiring connections

PCB file

EagleCAD PCB board file.

Code

HX711

Python
HX711 library
# hx711f.py
# Modulae and class for 24-Bit ADC HX711
# Datasheet: https://datasheetspdf.com/pdf-file/842201/Aviasemiconductor/HX711/1
# changes for english and auto calibration
# 
from time import sleep_us, ticks_ms

class DeviceNotReady(Exception):
    def __init__(self):
        print("HX711 not responding")


class HX711(DeviceNotReady):
    KselA128 = const(1)
    KselB32 = const(2)
    KselA64 = const(3)
    Dbits =const(24)
    MaxVal = const(0x7FFFFF)
    MinVal = const(0x800000)
    Frame = const(1<<Dbits)
    ReadyDelay = const(3000) # ms
    WaitSleep =const(60) # us
    ChannelAndGain={
        1:("A",128),
        2:("B",32),
        3:("A",64),
        }
    #KalibrierFaktor=400 #1104
    
    def __init__(self, dOut, pdSck, KalibrierFaktor, ch=KselA128):
        self.data=dOut
        self.data.init(mode=self.data.IN)
        self.clk=pdSck
        self.clk.init(mode=self.clk.OUT, value=0)
        self.channel=ch
        self.tare=0
        self.cal=KalibrierFaktor
        self.waitReady()
        k,g=HX711.ChannelAndGain[ch]
        print("HX711 channel ready {} with Gain {}".\
              format(k,g))
        
    def TimeOut(self,t):
        start=ticks_ms()
        def compare():
            return int(ticks_ms()-start) >= t
        return compare
    
    def isDeviceReady(self):
        return self.data.value() == 0
    
    def waitReady(self):
        delayOver = self.TimeOut(ReadyDelay)
        while not self.isDeviceReady():
            if delayOver():
                raise DeviceNotReady()
    
    def convertResult(self,val):
        if val & MinVal:
            val -= Frame
        return val
    
    def clock(self):
        self.clk.value(1)
        self.clk.value(0)
    
    def kanal(self, ch=None):
        if ch is None:
            ch,gain=HX711.ChannelAndGain[self.channel]
            return ch,gain
        else:
            assert ch in [1,2,3], "Wrong Channel number: {}\nShould be 1,2 3".format(ch)
            self.channel=ch
            if not self.isDeviceReady():
                self.waitReady()
            for n in range(Dbits + ch):
                self.clock()
                
    def getRaw(self, conv=True):
        if not self.isDeviceReady():
            self.waitReady()
        raw = 0
        for b in range(Dbits-1):
            self.clock()
            raw=(raw | self.data.value())<< 1 
        self.clock()
        raw=raw | self.data.value()
        for b in range(self.channel):
            self.clock()
        if conv:
            return self.convertResult(raw)

        else:
            return raw
    
    def mean(self, n):
        s=0
        for i in range(n):
            s += self.getRaw()
        return int(s/n) 
    
    def tara(self, n):
        self.tare = self.mean(n)
        return self.tare
        
    def masse(self,n):
        g=(self.mean(n)-self.tare) / self.cal
        return g
    
    def massraw(self,n):
        g=(self.mean(n)-self.tare)
        return g        
        
    def calFaktor(self, f=None):
        if f is not None:
            self.cal = f
        else:
            return self.cal
        
    def wakeUp(self):
        self.clk.value(0)
        self.kanal(self.channel)

    def toSleep(self):
        self.clk.value(0)
        self.clk.value(1)
        sleep_us(WaitSleep)

Filament Types

Plain text
List of filament types
Filament_Type, Density(kgm3)
PLA,1.24
ABS,1.04
ASA,1.07
PETG,1.27
Nylon,1.08
PC,1.20
HIPS,1.07
PVA,1.19
TPU,1.20
PMMA,1.18
Cu_Fill,3.90
Wood_Fill,1.28
C_Fibre,1.3
PP,0.9
Acetal,1.4

Filament Diameter

Plain text
List of filament diameters
Filament_Diameter(mm)
1.75
3

Spool Weights

Plain text
Empty Spool Weights
Supplier,Weight(g)
eSUN,224
Sunlu,133
Elegoo,162

Main Program

Python
Controlling Code
# Filament Scale cbf 2025
# Load Cell 5kg
# HX711 amplifier https://datasheetspdf.com/pdf-file/842201/Aviasemiconductor/HX711/1
# HX711.py https://grzesina.de/az/waage/hx711.py
# RP2040 lcd 0.96 https://files.waveshare.com/upload/2/28/Pico_code.7z
# MicroPython v1.15 on 2021-04-18 https://files.waveshare.com/upload/5/51/Rp2-pico-20210418-v1.15.7z
# Text files containing Filament and spool data - ftypes.txt, fdia.txt & fspool.txt

import os
import sys
from machine import Pin
import time
import math

# HX711 setup
from hx711 import HX711

#LCD setup
#color is BGR
RED = 0x00F8
GREEN = 0xE007
BLUE = 0x1F00
WHITE = 0xFFFF
BLACK = 0x0000
YELLOW = 0x00FF

from lcd096 import LCD_0inch96
lcd = LCD_0inch96()

# Button assignments
Button_A = Pin(18, Pin.IN, Pin.PULL_DOWN) # Filament option
Button_B = Pin(19, Pin.IN, Pin.PULL_DOWN) # Diameter option
Button_C = Pin(20, Pin.IN, Pin.PULL_DOWN) # Spool
Button_D = Pin(21, Pin.IN, Pin.PULL_DOWN) # Scale Mode 0=Filament, 1=Standard
# If power is applied with Button_D = 0 starts as Filament Scale, if power is applied with Button_D = 1 start as Standard Scale.

#default values applied if user file is not available or usable 
fdensity = [["PLA",1.24],["ABS",1.04],["ASA",1.07],["PETG",1.27],["Nylon",1.08],["PC",1.20],["HIPS",1.07],["PVA",1.19],["TPU",1.20],["PMMA",1.18],["Cu_Fill",3.90],
           ["Wood_Fill",1.28],["C_Fibre",1.3],["PP",0.9],["Acetal",1.4]] # ["Filament Type", Density(cm3)]

fdiameter = [1.75, 3.00] # filament diameter (mm)

fspool = [["eSUN",224],["Sunlu",133],["Elegoo",162]] # ["Supplier",Weight(g)]
#

sum_filcount = -1
sum_diacount = -1
sum_spoolcount = -1
gcm3 = 0 # filament density
fspogr = 0 # filament spool weight
fdia = 0 # filament diameter
Smode = 0

def FileOpen(fname):  
    array = []
    try:
        os.stat(fname) # validate file
        error = False
    except OSError:    
        # print("File error")
        error = True         
    else:    
        # Open the file and read into array
        f = open(fname, 'rt')
        with f as file:
            next(file) # skip header in file
            array = [[n for n in line.strip().split(",")] for line in file]
 
        # print(array)

    return error, array

def Volume(ngrams, density):
    # Volume(cm3) = Weight(g)/Density(g/cm3)
    if (density > 0):
        V = ngrams/density 
    else:
        V = 0
    return V

def Length(Vlm, dia):
    # Length(m) = Volume/(((Diameter/2)^2)*pi 
    if (Vlm > 0):
        L = Vlm/(((dia/2)**2)*math.pi) 
    else:
        L = 0
    return L

def menu1():
    lcd.text("Press option button",5,15,GREEN) 
    lcd.text("A:Filament Scale",5,27,GREEN)
    lcd.text("B:Standard Scale",5,37,GREEN)
    lcd.display()
    return

def menu2():
    lcd.text("Filament Scale",25,15,GREEN)
    lcd.text("A:Filament=",5,27,GREEN) # Filament Type & density (g/cm3)
    lcd.text("B:Diameter=",5,37,GREEN) # Filament Diameter (mm)
    lcd.text("C:Spool   =",5,47,GREEN) # Spool weight (g)
    lcd.display()
    return

def Filament():
    # select filament and density in list
    global sum_filcount, fdensity, gcm3
    sum_filcount += 1
    if sum_filcount >= len(fdensity):
        sum_filcount = -1
    fname = fdensity[sum_filcount] # filament type
    gcm3 = float(fname[1]) # filament density
    # print(gcm3)
    lcd.fill_rect(95,27,64,8,BLACK) # x,y,width,height,colour
    lcd.text(fname[0][0:9],95,27,GREEN) # text,x,y,colour
    lcd.display()
    return

def Diameter():
    # select filament diameter in list
    global sum_diacount, fdiameter, fdia
    sum_diacount += 1
    if sum_diacount >= len(fdiameter):
        sum_diacount = -1
    tmp = fdiameter[sum_diacount] # filament diameter

    try:
        fdia = float(tmp[0]) # file value
        # print ("file",tmp, fdia)
    except:
        fdia = tmp # internal value
        # print ("def",tmp, fdia) 
    
    lcd.fill_rect(95,37,64,8,BLACK)
    lcd.text(str(fdia)+"mm",95,37,GREEN)
    lcd.display()
    return

def Spool():
    # select spool
    global sum_spoolcount, fspool, fspogr
    sum_spoolcount += 1
    if sum_spoolcount >= len(fspool):
        sum_spoolcount = -1
    fspo = fspool[sum_spoolcount] # spool name
    fspogr = float(fspo[1]) # spool weight
    # print (fspogr)
    lcd.fill_rect(95,47,64,8,BLACK) # x,y,width,height,colour 
    lcd.text(fspo[0][0:4]+":"+str(fspo[1])+"g",95,47,GREEN) # text(manu:g),x,y,colour
    lcd.display()
    return

def Weigh():
    global fspogr, hx
    m=hx.masse(10)
    # print(m, fspogr)

    state=(test.value() == 0)
    time.sleep(0.5)
    
    if m <= 1:
        m = 0
    
    if (fspogr > 0):
        grams = m - fspogr
    else:
        grams = m
    return grams

def Results():
    global gcm3, fdia, fspogr
    NetWeight = float(Weigh())
    if (fspogr > 0):
        # Filament scale mode
        Vol = Volume(NetWeight, gcm3)
        Metres = round(Length(Vol, fdia), 3)
        lcd.fill_rect(5,67,160,8,BLACK) # x,y,width,height,colour       
        lcd.text(str(Metres)[0:7]+"m",5,67,GREEN)
        lcd.text(str(NetWeight)[0:7]+"g",80,67,GREEN)
        lcd.display()
    else:
        # Standard scale mode
        lcd.fill(BLACK)
        lcd.text("Standard Scale",24,20,GREEN)
        lcd.text(str(NetWeight)[0:7]+"g",26,50,GREEN)
        lcd.display()
    return
  
lcd.fill(BLACK)
lcd.display()

lcd.text("Remove weight now!",5,20,GREEN)
# countdown delay
for i in range(10, -1, -1):
    lcd.fill_rect(5,40,160,8,BLACK) # x,y,width,height,colour
    lcd.text(str(i)+" Sec",60,40,GREEN)
    lcd.display()
    time.sleep(1)
    
lcd.fill(BLACK)

# HX711 Initilization 
dout=Pin(0)
dpclk=Pin(1)
delta = 0 # weight offset adjustment
test=Pin(0,Pin.IN,Pin.PULL_UP)

try: 
    lcd.text("Initializing",24,40,GREEN)
    lcd.display()
   
    hx = HX711(dout,dpclk,405) # third passing parameter (numeric), is the calibration factor found using Calscale.py
    hx.wakeUp()
    hx.kanal(1)
    hx.tara(25)
    hx.calFaktor(hx.calFaktor()+delta)
    # print("Start")

except:
    # print("HX711 failed")
    lcd.fill(BLACK)
    lcd.text("HX711 failed",24,42,RED)
    lcd.display()
    sys.exit() 
#

status, filearr = FileOpen("ftypes.txt")
if status == False:
    fdensity = []
    fdensity = filearr 

status, filearr = FileOpen("fspool.txt")
if status == False:
    fspool = []
    fspool = filearr

status, filearr = FileOpen("fdia.txt")
if status == False:
    fdiameter = [] 
    fdiameter = filearr  

if __name__=='__main__':   
    
    Button = 0
    lcd.fill(BLACK)
    menu1()
    while True:    
        # Filament Mode        
        if Button_A.value() == 1:
            Smode = 1 
            B1 = 0; B2 = 0; B3 = 0
            time.sleep(1) # Button_A RTZ
            lcd.fill(BLACK)
        
        # Standard Mode        
        if Button_B.value() == 1:
            Smode = 2
            lcd.fill(BLACK)
        
        if Smode == 1:         
            menu2()
            if Button_D.value() == 0:
                if Button_A.value() == 1:
                    B1 = 1
                    Filament()
                    time.sleep(1)
            
                if Button_B.value() == 1:
                    B2 = 1
                    Diameter()
                    time.sleep(1)

                if Button_C.value() == 1:
                    B3 = 1
                    Spool()
                    time.sleep(1)
                
                Button = B1 + B2 + B3
                
                if Button == 3:
                    lcd.text("Set D to Weigh",5,57,YELLOW)
                    
            else:
                if Button == 3:
                    lcd.fill_rect(5,57,160,8,BLACK) 
                    Results()      
        
        if Smode == 2:
            if Button_D.value() == 1:
                Results()
            else:
                lcd.text("Standard Scale",24,20,GREEN)
                lcd.text("Set D to Weigh",24,30,YELLOW)
                lcd.display()

LCD

Python
LCD096 library file
# lcd096.py
# Module and Class for LCD_0.96 16080 pixels 65K colorful IPS LCD display

from machine import Pin,SPI,PWM,freq
import framebuf
import time

#color is BGR
RED = 0x00F8
GREEN = 0xE007
BLUE = 0x1F00
WHITE = 0xFFFF
BLACK = 0x0000

class LCD_0inch96(framebuf.FrameBuffer):
    def __init__(self):
      
        self.width = 160
        self.height = 80
        self.cs = Pin(9,Pin.OUT)
        self.rst = Pin(12,Pin.OUT)
#        self.bl = Pin(13,Pin.OUT)
        self.cs(1)
        # pwm = PWM(Pin(13))#BL
        # pwm.freq(1000)        
        self.spi = SPI(1)
        self.spi = SPI(1,1000_000)
        self.spi = SPI(1,10000_000,polarity=0, phase=0,sck=Pin(10),mosi=Pin(11),miso=None)
        self.dc = Pin(8,Pin.OUT)
        self.dc(1)
        self.buffer = bytearray(self.height * self.width * 2)
        super().__init__(self.buffer, self.width, self.height, framebuf.RGB565)
        self.Init()
        self.SetWindows(0, 0, self.width-1, self.height-1)
        
    def reset(self):
        self.rst(1)
        time.sleep(0.2) 
        self.rst(0)
        time.sleep(0.2)         
        self.rst(1)
        time.sleep(0.2) 
        
    def write_cmd(self, cmd):
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))

    def write_data(self, buf):
        self.dc(1)
        self.cs(0)
        self.spi.write(bytearray([buf]))
        self.cs(1)

    def backlight(self,value):#value:  min:0  max:1000
        pwm = PWM(Pin(13))#BL
        pwm.freq(1000)
        if value>=1000:
            value=1000
        data=int (value*65536/1000)       
        pwm.duty_u16(data)  
        
    def Init(self):
        self.reset() 
        self.backlight(10000)  
        
        self.write_cmd(0x11)
        time.sleep(0.12)
        self.write_cmd(0x21) 
        self.write_cmd(0x21) 

        self.write_cmd(0xB1) 
        self.write_data(0x05)
        self.write_data(0x3A)
        self.write_data(0x3A)

        self.write_cmd(0xB2)
        self.write_data(0x05)
        self.write_data(0x3A)
        self.write_data(0x3A)

        self.write_cmd(0xB3) 
        self.write_data(0x05)  
        self.write_data(0x3A)
        self.write_data(0x3A)
        self.write_data(0x05)
        self.write_data(0x3A)
        self.write_data(0x3A)

        self.write_cmd(0xB4)
        self.write_data(0x03)

        self.write_cmd(0xC0)
        self.write_data(0x62)
        self.write_data(0x02)
        self.write_data(0x04)

        self.write_cmd(0xC1)
        self.write_data(0xC0)

        self.write_cmd(0xC2)
        self.write_data(0x0D)
        self.write_data(0x00)

        self.write_cmd(0xC3)
        self.write_data(0x8D)
        self.write_data(0x6A)   

        self.write_cmd(0xC4)
        self.write_data(0x8D) 
        self.write_data(0xEE) 

        self.write_cmd(0xC5)
        self.write_data(0x0E)    

        self.write_cmd(0xE0)
        self.write_data(0x10)
        self.write_data(0x0E)
        self.write_data(0x02)
        self.write_data(0x03)
        self.write_data(0x0E)
        self.write_data(0x07)
        self.write_data(0x02)
        self.write_data(0x07)
        self.write_data(0x0A)
        self.write_data(0x12)
        self.write_data(0x27)
        self.write_data(0x37)
        self.write_data(0x00)
        self.write_data(0x0D)
        self.write_data(0x0E)
        self.write_data(0x10)

        self.write_cmd(0xE1)
        self.write_data(0x10)
        self.write_data(0x0E)
        self.write_data(0x03)
        self.write_data(0x03)
        self.write_data(0x0F)
        self.write_data(0x06)
        self.write_data(0x02)
        self.write_data(0x08)
        self.write_data(0x0A)
        self.write_data(0x13)
        self.write_data(0x26)
        self.write_data(0x36)
        self.write_data(0x00)
        self.write_data(0x0D)
        self.write_data(0x0E)
        self.write_data(0x10)

        self.write_cmd(0x3A) 
        self.write_data(0x05)

        self.write_cmd(0x36)
        self.write_data(0xA8)

        self.write_cmd(0x29) 
        
    def SetWindows(self, Xstart, Ystart, Xend, Yend):#example max:0,0,159,79
        Xstart=Xstart+1
        Xend=Xend+1
        Ystart=Ystart+26
        Yend=Yend+26
        self.write_cmd(0x2A)
        self.write_data(0x00)              
        self.write_data(Xstart)      
        self.write_data(0x00)              
        self.write_data(Xend) 

        self.write_cmd(0x2B)
        self.write_data(0x00)
        self.write_data(Ystart)
        self.write_data(0x00)
        self.write_data(Yend)

        self.write_cmd(0x2C) 
        
    def display(self):
        self.SetWindows(0,0,self.width-1,self.height-1)       
        self.dc(1)
        self.cs(0)
        self.spi.write(self.buffer)
        self.cs(1)  

Credits

C Forde
12 projects • 3 followers
Thanks to J.Grzesina.

Comments