#!/usr/bin/python3 """ routine to handle weighing filament spool holder uses hx711 library from https://github.com/tatobari/hx711py does IQR-based filtering of outliers in read data reads and writes from a state file usage: weigh synonym for weigh filament weigh f[ilament] report weight of filament on spool on holder weigh c[alibrate] calibrate scale by weighing a known weight weigh g[ross] report total weight on holder weigh m[anual] doesn't weigh anything, but set weight of an empty spool weigh q[uickly] quickly / quietly report weight of filament on spool on holder weigh r[aw] report raw readings with no chit-chat weigh s[pool] record weight of an empty spool weigh z[ero] record reading with empty holder (no spool) """ import time import sys import re import hx711 import numpy as np from RPi.GPIO import cleanup # parameters for the HX711 # HX711 DAT line GPIO hxdat=5 # HX711 CLK line GPIO hxclk=6 # HX711 bytes order hxbyt="MSB" # HX711 bits order hxbit="MSB" # HX711 gain - 128, 64 or 32 # 128 or 64 force channel A, 32 forces channel B hxgain=128 # parameters for weight reads # number of reads for spool, calibrate and zero operations calreads=99 # number of reads for weighing operations wreads=20 # reads if done quickly qwreads=5 # we search for outliers by reference to distance from first and third quartile # as a function of teh inter-quartile range IQR # this defines distance from teh relevant qaurtile of the fence # set = 0 means we discard half teh data and use only teh data within teh IQR (wasteful) # set = 1.5 is generally a fairly near limit # with large number of samples a further fence may be appropriate fencedist=2.5 # weight unit # this actually doesn't matter - it's just a label # you can enter weights in any units, as long as the weights entered are consistent # the unit label must not start with a digit or '.' wunit='g' # location of the state file # this is a miscellaneous variable state record, so ought to be in /var/lib/misc/ # but can go anywhere # it needs to be writable statefile = '/var/lib/misc/spoolweigh.txt' # read and memorise state file values zz = None cc = None ss = None # this is spool weight in raw units sw = None # this is spool weight in weight units with open (statefile, 'r') as f: for line in f: match = re.match('\d+ : zero ([-\.\d]+)',line) if match: zz = float(match.group(1)) match = re.match('\d+ : calib ([-\.\d]+) = ([\.\d]+)',line) if match: cc = float(match.group(1))/float(match.group(2)) match = re.match('\d+ : spool ([-\.\d]+)',line) if match: ss = float(match.group(1)) sw = None match = re.match('\d+ : spoolwt ([-\.\d]+)',line) if match: sw = float(match.group(1)) ss = None # set ss (spool weight in raw units) from sw (manual spool weight) or vice-versa if sw is not None: ss = sw * cc if ss is not None: sw = ss / cc # bail out if we have insufficient information from state file # this is not very pythonic, but I want to know we have all the necessary # information before doing a time-consuming weighing loop # first letter of first major option, drop any leading '-'s and capitalise try: opt = sys.argv[1].strip('-').upper()[0] except IndexError: # assume we had no options so treat that as a 'weigh filament' opt = 'F' if zz is None: # zero is needed for all operations except zero and manual if opt in 'GQFSC': print ('No zero tare value found - run \'weigh zero\' first'); sys.exit() if cc is None: # calibration is needed for filament weighing if opt in 'GQF': print ('No calibration value found - run \'weigh calibrate\' first'); sys.exit() if ss is None and sw is None: # need either type of spool weight for filament weighing if opt in 'QF': print ('No spool weight value found - run \'weigh spool\' first'); sys.exit() # interact with the user if necesary if opt in 'ZCS': # doing a calibration type read - these ask for confirmation if opt == 'Z': print ("this will set a zero-point tare value") print ("ensure nothing is on spool holder") go = input("proceed? ") if opt == 'S': print ("this will set an empty spool tare value") print ("ensure an empty spool is on spool holder") go = input("proceed? ") if opt == 'C': print ("this will set a weight calibration value") # see if we have claibration value on command line try: ww=float(sys.argv[2]) print ('ensure {:.2f}{} weight is on spool holder'.format(ww,wunit)) go = input("proceed? ") except (IndexError,ValueError): print ("ensure a known weight is on spool holder") go = input('weight on holder (in ' + wunit + ')? ') try: ww = float(go) go = 'y' except ValueError: go = 'n' readings = calreads if opt in 'GF': # for a filament weigh we just get on with it, but tell the user print ("weighing...", end='\r') go = 'y' readings = wreads if opt in 'Q': # for a quick / quiet filament weigh we don't even tell the user go = 'y' readings = qwreads if opt in 'R': # for a raw read we have mninimal chat and just go go = 'y' readings = calreads if opt == 'M': # this doesn't actually require any weighing print ('manually set empty spool weight') go = input('weight of spool (in ' + wunit + ')? ') try: ww = float(go) with open (statefile, 'a') as f: print (str(int(time.time()))+' : ', file=f, end='') print ('spoolwt {:.2f} {}'.format(ww,wunit), file=f, end='') print (' :',time.strftime('%Y/%m/%d %H:%M:%S'), file=f, end='') print (' : spool weight set manually', file=f) except ValueError: pass # this option doesn't do any weighing go = 'n' if go.upper()[0] == 'Y': # initialise sensor hx = hx711.HX711(hxdat, hxclk) hx.set_reading_format(hxbyt, hxbit) hx.set_gain(hxgain) # since we read raw long values this is probably redundant hx.set_reference_unit(1) # reset chip - not sure if this really necesary hx.reset() time.sleep(0.1) # take specified number of readings and average them # empty list of readings vals=np.empty(readings) # take readings for i in range(readings): read = hx.read_long() if opt in 'ZCSR': print(read) vals[i-1]=read # chip off hx.power_down() # tidy up gpio cleanup() # find quartiles first=np.percentile(vals,25) third=np.percentile(vals,75) # place fences lowfence = first - fencedist * (third-first) highfence = third + fencedist * (third-first) # filter outliers filtered = vals[(vals > lowfence) * (vals < highfence)] # find average of what remains avg = np.mean(filtered) if opt in 'ZCS': # was a calibration-type read print('======') print('{:.2f} (from {} of {} readings)'.format(avg,len(filtered),len(vals))) with open (statefile, 'a') as f: print (str(int(time.time()))+' : ', file=f, end='') if opt == 'Z': print ('zero {:.2f}'.format(avg)) print ('zero {:.2f}'.format(avg), file=f, end='') if opt == 'C': print ('calib {:.2f} = {:.2f}{}'.format(avg-zz, ww, wunit)) print ('calib {:.2f} = {:.2f} {}'.format(avg-zz, ww, wunit), file=f, end='') if opt == 'S': if cc: print ('spool {:.2f} = {:.2f}{}'.format(avg-zz,(avg-zz)/cc,wunit)) print ('spool {:.2f} = {:.2f} {}'.format(avg-zz,(avg-zz)/cc,wunit), file=f, end='') else: print ('spool {:.2f}'.format(avg-zz)) print ('spool {:.2f}'.format(avg-zz), file=f, end='') print (' :',time.strftime('%Y/%m/%d %H:%M:%S'), file=f, end='') print (' : raw={:.2f}'.format(avg), file=f, end='') if zz: print (' zz={:.2f}'.format(zz), file=f, end='') if cc: print (' cc={:.2f}'.format(cc), file=f, end='') if ss: print (' ss={:.2f}'.format(ss), file=f, end='') if sw: print (' sw={:.2f}{}'.format(sw,wunit), file=f, end='') print(file=f) if opt == 'G': # gross read print ('{:.2f}{} gross'.format((avg-zz)/cc, wunit)) if opt in 'QF': # net read - need to know spool weight # we shoudl have either ss (spool weight in raw) or sw (spool weight in wunit) if sw is not None: #set ss (spool weight in raw units) from sw (manual spool weight) ss=sw*cc if opt == 'Q': print ('{:.2f}'.format((avg-zz-ss)/cc, wunit)) if opt == 'F': print ('{:.2f}{} filament'.format((avg-zz-ss)/cc, wunit)) # pack up sys.exit()