Files
2026-08-20 06:17:39 +08:00

832 lines
31 KiB
Python
Executable File

#!/usr/bin/python3
# Note: '#!/usr/bin/python3' is used instead of
# '#!/usr/bin/env python3'
# since it allows ps and pidof to show/find
# using the script name
# Example [EMCIO]DB_PROGRAM:
# Demonstrate LinuxCNC interface for a database of tools
# with tool time usage monitoring
# ini file examples:
# [EMCIO]DB_PROGRAM=./db_nonran.py [nonran_filename]
# [EMCIO]DB_PROGRAM=./db_ran.py [ran_filename]
# defaults for *_filename:
db_ran_savefile = "/tmp/db_ran_file" # db flatfile
db_nonran_savefile = "/tmp/db_nonran_file" # db flatfile
#-----------------------------------------------------------
import os
import sys
import time
import signal
import threading
import getopt
import linuxcnc
#-----------------------------------------------------------
# LinuxCNC tooldb module for low-level interface:
from tooldb import tooldb_callbacks # functions (g,p,l,u)
from tooldb import tooldb_tools # list of tool numbers
from tooldb import tooldb_loop # main loop
#-----------------------------------------------------------
# Notes
# 1) Uses the LinuxCNC provided tooldb module: so all
# writes to stdout are piped to LinuxCNC host.
# Use stderr for debug prints in this file.
# 2) tool usage time is computed based on
# loads/unloads. Tool must be unloaded
# when LinuxCNC is stopped.
# 3) a database savefile will be created at
# startup if it does not exist applying
# rules for a simulation setup.
# 4) startup/tooltimer
# a) random toolchanger: both LinuxCNC and database
# know if there is a loaded tool at startup so
# if a tool is loaded, timer is started
# b) nonrandom toolchanger: LinuxCNC is not
# aware of loaded tool at startup so timer is
# not started. (use m61qn to specify tool
# and start timer)
# c) best practice: operator always unloads tool
# when stopping LinuxCNC
# 5) The cmd_loop() function is executed in a thread
# so that concurrent tasks can be demonstrated.
# 6) Worker tasks demonstrate changes to database
# with synchronization to the LinuxCNC host:
# a) tool_modify_task: change a tool parameter
# b) tool_update_task add/remove tool no.s
# The tasks are executed in threads concurrent
# to the primary task that executes tooldb_loop().
# to illustrate methods for db_program guis that
# update and synchronize tooldata with LinuxCNC.
# 7) The periodic_task updates tool time usage.
# 8) A single mutex is used to protect all activity
# than can read/write tools[] and db_*_savefile
# The mutex is acquired and held by
# a) each tooldb interface routine specified
# by the call to tooldb_callbacks()
# b) the periodic_task
# c) each worker demonstration task:
# tool_modify_task()
# tool_update_task()
# 9) Monitor tool data updates by using shell watch
# command in a terminal. Examples:
# $ watch -d -n 1 cat /tmp/db_ran_file
# $ watch -d -n 1 cat /tmp/db_nonran_file
# 10) export environmental variables DB_SHOW, DB_DEBUG
# to print operational details from LinuxCNC
# 11) If LinuxCNC is not running, use is limited to basic
# command line testing for commands g,p,l,u.
# Use Ctrl-C to exit
# Example (simple non random toolchanger):
# $ ./db_nonran.py [debug]
# g (g: get all tools)
# p t11 p111 d.3 (p: put t11 for received update)
# g (g: note changes from p cmd)
# l t14 p0 (l: load tool 14)
# u t0 p0 (u: unload tool 14)
# Example (more complicated random toolchanger):
# $ ./db_ran.py [debug]
# $ g (g: get all tools)
# $ u t0 p114 (u: first of pair to load t14)
# $ l t14 p0 (l: second of pair to load t14)
# 12) problem: unexpected termination (like a power
# failure) is not detected by LinuxCNC so tool
# time data and tool-in-spindle status may be
# corrupted for such events. A database application
# may be able to detect and notify for some anomalies.
#-----------------------------------------------------------
# globals init
prog = sys.argv[0] # this program
toollist = [] # list of tool numbers
tools = dict() # tools[tno]: dict of text toollines
spindle_tool = -1 # no tool in spindle is indicated by:
# nonran:-1, ran:-1|0
nonran_from_pocket = dict()# use to restore pocket in db
start_time = -1 # neg means timer not active
elapsed_time = 0 # tool time usage
mfmt = "%.3f" # format for M letter usage time
sync_fail_reason = "" # why sync failed
cmdline_only = 0 # 1: for cmdline debugging
history = []; # history logging
history_max_ct = 10 # history logging
period_minutes = 1 # default for periodic_task
mutex = threading.Lock() # lock for all threads
disconnect_evt = threading.Event() # set at disconnect
# tletters: interface to LinuxCNC
tletters = ['T','P', 'D','X','Y','Z','A','B','C','U','V','W','I','J','Q']
# aux_letters: augment db usage
aux_letters = ['M'] # M for operating Minutes
all_letters = tletters + aux_letters
alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#-----------------------------------------------------------
# messages to stderr (stdout is piped to LinuxCNC)
def msg(txt): sys.stderr.write("%s: %s\n"%(prog,txt))
def umsg(txt): msg("UNEXPECTED: %s"%txt) # debug usage
try: # environ override default period_minutes for testing:
period_minutes = float(os.environ['DB_PERIOD_MINUTES'])
msg("period_minutes=%f"%period_minutes)
except Exception as e: pass
#-----------------------------------------------------------
# LinuxCNC interface for status & syncing
linuxcnc_stat = linuxcnc.stat()
linuxcnc_tool_sync = linuxcnc.command().load_tool_table
try: linuxcnc_stat.poll() # test if LinuxCNC running
except Exception as e:
msg("e=%s"%e)
msg("LinuxCNC not active, cmdline debugging")
cmdline_only = 1
#-----------------------------------------------------------
def ctrlc_handler(signo, frame):
sys.stderr.write(" Caught signo=%d Ctrl-C bye\n"%signo)
sys.exit(0)
if cmdline_only: signal.signal(signal.SIGINT, ctrlc_handler)
# defer handler until ready to avoid signal to io process
def notready_for_signal(signo, frame): umsg("notready_for_signal %d"%signo)
signal.signal(signal.SIGUSR1,notready_for_signal)
signal.signal(signal.SIGUSR2,notready_for_signal)
#-----------------------------------------------------------
# Simulation initial tool number and pocket number setups
toolno_min = 10
toolno_max = 19
pocket_offset = 100
n_extra_pockets = 5 # for add/remove demo
n_pockets = (toolno_max - toolno_min + 1) + n_extra_pockets
pockets_base = toolno_min + pocket_offset # start of pocket numbering
available_pockets = []
for i in range(n_pockets): available_pockets.append(pockets_base + i)
#-----------------------------------------------------------
def start_db_program():
global db_savefile, period_minutes, random_toolchanger
# Default: support random_toolchanger
# Use symbolic link with "nonran" in its name to implement
# a nonrandom_toolchanger
if sys.argv[0].find("nonran")>=0:
msg("starting(nonrandom_toolchanger)")
random_toolchanger = 0
db_savefile = db_nonran_savefile # default file
else:
msg("starting(random_toolchanger) n_pockets = %d"%n_pockets)
random_toolchanger = 1
db_savefile = db_ran_savefile # default file
# handle options and args:
try: opts,remainder = getopt.getopt(sys.argv[1:]
,shortopts='hp:'
,longopts=['help','period_minutes=']
)
except getopt.GetoptError as emsg:
msg("GetoptError:%s"%emsg)
sys.exit(1)
for opt,arg in opts:
if opt in ('-p','--period_minutes'):
period_minutes=float(arg)
msg("period_minutes=%f"%period_minutes)
if opt in ('-h','--help'):
msg("""
Usage: %s [Options] [flat_filename]
Default flat_filename = %s
Options:
-h|--help (and exit)
-p|--period_minutes value (default=%f)
bye!"""%(prog,db_savefile,period_minutes))
sys.exit(1)
if remainder:
db_savefile = remainder[0] # superseded default
msg("db_savefile = %s"%db_savefile)
if len(remainder) >1:
msg("Unknown arguments %s"%remainder[1:])
sys.exit(1)
#-----------------------------------------------------------
# functions
def toolline_to_list(line): return line.upper().split()
def toolline_to_dict(line,letters):
# line: "Tt Pp Dd Xx Yy Zz ..."
# dict: D["T"]=tno, D["P"]="p", D["D"]=d, ...
D = dict()
# use leading ';' to denote comment line
line_nocomment = line.upper().split(';')[0]
for item in line_nocomment.split():
for l in letters:
if l in item: D[l]=item.lstrip(l).lstrip('+')
return D
def dict_to_toolline(D,letters):
toolline = str()
for l in letters:
if l in D: toolline = toolline + " " + l + D[l]
toolline = toolline.strip()
return toolline
def save_tools_to_file(fname,comment=""):
f = open(fname,"w")
for tno in tools:
if debug: msg("@@Save: tno=%d %s"%(tno,tools[tno]))
D = toolline_to_dict(tools[tno],all_letters)
for l in all_letters:
if l == "T" or l == "P": continue # always include
if l in D and D[l] == "0": del D[l] # drop zero entries
tools[tno] = dict_to_toolline(D,all_letters)
for i in sorted(tools,key=tools.get):
f.write(tools[i] + " ;" + tool_comment(i) + "\n")
global history
if comment: history.append(comment)
if len(history) > history_max_ct: history=history[1:]
if len(history):
f.write("\n; Recent (last %d) db history:\n"%history_max_ct)
for i in range(len(history)):
f.write("; %s\n"%history[i])
f.close()
def nonran_pno(tno): # simulation rule:
return tno+pocket_offset # make startup pocket number different than tool number
# to avoid assumptions about tno and pno
def nonran_restore_pocket(tno):
if not tno in nonran_from_pocket:
umsg("nonran_restore_pocket tno=%d not in dict: %s"%(tno,nonran_from_pocket))
D = toolline_to_dict(tools[tno],all_letters)
D['P'] = nonran_from_pocket[tno]
tools[tno] = dict_to_toolline(D,all_letters)
#msg("Restore pocket tno:%d pno %s"%(tno,int(D['P'])))
del nonran_from_pocket[tno]
def assign_pocket(tno):
global available_pockets
if not random_toolchanger:
return nonran_pno(tno)
if not len(available_pockets):
umsg("assign_pocket: no pockets available") # expect error
available_pockets.sort()
pno = available_pockets[0]
available_pockets.remove(pno)
return pno
def release_pocket(pno):
global available_pockets
if not random_toolchanger: return # not applicable
if pno in available_pockets: umsg("pno=%d was already available"%pno)
available_pockets.append(pno)
available_pockets.sort()
return
def init_tools(tfirst,tlast):
# set tool parameters for all tools
global tools
msg("init_tools(): tfirst=%d tlast=%d"%(tfirst,tlast))
for tno in range(tfirst,tlast+1):
if random_toolchanger:
pno = assign_pocket(tno)
tools[tno] = "T%-3d P%-3d Z0.%d D0.%d"%(tno,pno,tno,tno)
tools[0] = "T0 P0 ; Empty pocket"
else:
tools[tno] = "T%-3d P%-3d Z0.%d D0.%d"%(tno,nonran_pno(tno),tno,tno)
# for testing tool time usage computations:
# T11,T12 have same diameter as T10
D = toolline_to_dict(tools[11],all_letters)
D['D'] = "D0.10"
tools[11] = dict_to_toolline(D,all_letters)
D = toolline_to_dict(tools[12],all_letters)
D['D'] = "D0.10"
tools[12] = dict_to_toolline(D,all_letters)
save_tools_to_file(db_savefile,"Created %s (init_tools)"%db_savefile)
return
def make_tools(tfirst,tlast):
global tools,spindle_tool
try: f = open(db_savefile,"r")
except IOError:
init_tools(tfirst,tlast)
initialtoollist = list(range(toolno_min,toolno_max+1))
if random_toolchanger: initialtoollist.append(0) # T0 provision
return initialtoollist
except Exception as e:
msg("make_tools: e=%s"%e)
# use preexisting file:
savedtoollist = []
while True:
line = f.readline().strip()
if not line: break
if line[0] == ';': continue
D = toolline_to_dict(line,all_letters)
tno = int(D["T"])
savedtoollist.append(tno)
if random_toolchanger:
pno = int(D["P"])
if pno != 0: available_pockets.remove(pno)
if not random_toolchanger:
#Note: nonran toolchanger:
# database may indicate tool is loaded at startup
# but LinuxCNC does not have this info
# so: timer is not started, use nonran_pno
if D["P"] == "0":
msg("nonran toolchanger was stopped with loaded tool %d"%tno)
D["P"] = str(nonran_pno(tno))
tools[tno] = dict_to_toolline(D,all_letters)
if debug: msg("@@tno=%d line=%s"%(tno,line))
if random_toolchanger and D['P'] == "0":
spindle_tool = tno
start_tool_timer(spindle_tool)
f.close()
save_tools_to_file(db_savefile
,"Using preexisting db file: %s (make_tools)"%db_savefile)
return savedtoollist
def sync_allowed():
global sync_fail_reason
try:
linuxcnc_stat.poll()
if linuxcnc_stat.interp_state != linuxcnc.INTERP_IDLE:
sync_fail_reason = "interp not idle"
return 0 # fail
except Exception as e:
msg("sync_allowed fail:%s"%e)
sync_fail_reason = e
return 0 # fail
sync_fail_reason = ""
return 1 # success
def add_dbtool(tno,**kwargs):
# kwargs specify parameters in tletters
# example usage: add_dbtool(tno,Ppvalue Zzvalue Ddvalue Xxvalue ...)
global toollist,tools
for k in kwargs.keys():
if k != k.upper():
msg("!!!add_dbtool ERR: unexpected lower case param: %s"%k)
if not k in tletters:
msg("!!!add_dbtool ERR: unknown param letter: %s"%k)
return 0 # fail
if not sync_allowed():
msg("add_dbtool: DISALLOWED tno=%d (%s)"%(tno,sync_fail_reason))
return 0 #fail
parms=""
for l in tletters:
if l in kwargs:
if l == 'P': umsg("add_dbtool tno=%d P%s"%(tno,kwargs[l]))
parms = parms + " %s%s"%(l,kwargs[l])
pno = assign_pocket(tno)
tools[tno] = "T%d"%tno + " P%d"%pno + parms
toollist.append(tno)
save_tools_to_file(db_savefile,"tno:%3d added (add_dbtool)"%tno)
tooldb_tools(toollist); linuxcnc_tool_sync()
msg("add_dbtool: ADD tno=%d pno=%d"%(tno,pno))
return 1 # success
def rm_dbtool(tno):
if not tno in toollist:
msg(" rm_dbtool: ERR tno=%d not in toollist"%tno)
return 0 # fail
if tno == spindle_tool:
msg(" rm_dbtool: ERR tno=%d is loaded to spindle"%tno)
return 0 # fail
D = toolline_to_dict(tools[tno],tletters)
pno = int(D['P'])
if not sync_allowed():
msg(" rm_dbtool: DISALLOWED tno=%d (%s)"%(tno,sync_fail_reason))
return 0 #fail
del tools[tno]
toollist.remove(tno)
save_tools_to_file(db_savefile,"tno:%3d removed (rm_dbtool)"%tno)
tooldb_tools(toollist); linuxcnc_tool_sync()
msg(" rm_dbtool: RM tno=%d pno=%d"%(tno,pno))
return 1 # success
def handle_disconnect(fname):
global disconnect_evt
if spindle_tool > 0:
stop_tool_timer(spindle_tool)
msg("!!!Stopped with loaded tool: %d"%spindle_tool)
if not random_toolchanger:
nonran_restore_pocket(spindle_tool)
save_tools_to_file(fname,"tno:%3d in spindle at termination"%spindle_tool)
msg("disconnected")
disconnect_evt.set()
def start_tool_timer(tno):
global start_time, elapsed_time, spindle_tool
if tno != spindle_tool:
umsg("start_tool_timer tno %d != spindle_tool %d"%(tno,spindle_tool))
if start_time >= 0:
umsg("start_tool_timer: tno:%d spindle_tool:%d start_time=%f"%(tno,spindle_tool,start_time))
start_time = time.time()
def update_tool_time(tno):
global start_time, elapsed_time
now = time.time()
if tno == 0: umsg("update_tool_time with no tool")
if start_time < 0 and spindle_tool > 0:
umsg("update_tool_time spindle_tool=%d start_time=%f"%(spindle_tool,start_time))
start_time = now
return
elapsed_time = now - start_time
start_time = now
D = toolline_to_dict(tools[tno],all_letters)
if not 'M' in D:
D['M'] = mfmt%(elapsed_time/60.0)
else:
tot_time_m = float(D['M'].strip('M')) + elapsed_time/60.0
D['M'] = mfmt%(tot_time_m)
tools[tno] = dict_to_toolline(D,all_letters)
save_tools_to_file(db_savefile)
def stop_tool_timer(tno):
global start_time, elapsed_time, spindle_tool
if start_time < 0 and random_toolchanger and tno != 0:
# ran_tc and starting with tool in spindle
msg("NOTE: Tool %d was not unloaded in prior run"%tno)
return
if spindle_tool <= 0: umsg("stop_tool_timer with no loaded tool")
update_tool_time(spindle_tool)
start_time = -1 # negative means timer not active
def update_tool_params(tno,update_line):
D = toolline_to_dict(tools[tno],all_letters)
for item in toolline_to_list(update_line):
for l in tletters:
if l in item: D[l]=item.lstrip(l) #supersede by update_line
if not random_toolchanger and spindle_tool == tno:
D['P'] = '0' # unload uses nonran_from_pocket[]
tools[tno] = dict_to_toolline(D,all_letters)
def apply_db_rules():
# Apply database rules
# typically: when a tooldata reload requested
# alternately: ater a tool is unloaded
# Rule: For tools having identical diameter (within diameter_epsilon),
# lowest tool number is assigned to the pocket/parameters for
# the tool with the lowest operating time (in minutes).
#
# So if t10diameter==t11diameter==t12diameter,
# t10 will select tool with lowest operating minutes
# ...
# t12 will select tool with most operating minutes
#
# NOTE: Tooldata must be reloaded for retrieval of updates.
# Use gui reload buttons or G10L0 in mdi or gcode programs
#msg("APPLY_DB_RULES================================")
diameter_epsilon = 0.001
if spindle_tool > 0: # not allowed with G10L0 but gui may attempt
msg("DISALLOW: apply_db_rules with tool %d loaded"%spindle_tool)
return
for tno in toollist:
if tno == 0: continue
CUR = toolline_to_dict(tools[tno],all_letters)
diam = float(CUR["D"])
if not 'M' in CUR: CUR['M'] = "0"
# find tools after tno in toollist with same diameter:
for tryno in toollist[toollist.index(tno)+1:]:
if tryno == 0: continue
TRY = toolline_to_dict(tools[tryno],all_letters)
if abs(diam - float(TRY["D"])) > diameter_epsilon: continue
# found equal diameters
if not 'M' in TRY: TRY['M'] = "0"
#msg("diameter match: tno=%d tryno=%d diam=%f trydiam=%s"
# %(tno,tryno,diam,TRY["D"]))
if float(TRY['M']) < float(CUR['M']):
# swap tool data so that tno has fewest minutes:
CUR['T'] = str(tryno)
TRY['T'] = str(tno)
tools[tno] = dict_to_toolline(TRY,all_letters)
tools[tryno] = dict_to_toolline(CUR,all_letters)
CUR = toolline_to_dict(tools[tno],all_letters)
save_tools_to_file(db_savefile,
"tno:%3d diam=%f NEWpno=%s minutes=%s (apply_db_rules)"
%(tno,diam,CUR['P'],CUR['M']))
def check_params(tno,params):
if (tno != 0) and (not tno in tools): # expect NAK
umsg("Attempt to use tno=%d not in db"%tno)
for l in params.upper():
if l in alphabet and not l in tletters:
umsg("unknown param letter:%s in %s"%(l,params))
def tool_comment(tno):
from datetime import datetime as dt
return "Tool_%2d %s"%(tno,dt.today().strftime("%d%b%y:%H.%M.%S"))
#-----------------------------------------------------------
# Interface (callback) functions
# 'g' interface command (ran or nonran)
def user_get_tool(tno):
mutex.acquire() # blocking
# detect tool data reload (gui or with G10L0) and apply db rules:
if tno == toollist[0]: apply_db_rules()
if debug: msg("@@user_get_tool: %s"%tools[tno])
ans = "" # line formatted for LinuxCNC db interface
for item in toolline_to_list(tools[tno]):
for l in tletters:
if l in item: ans = ans + " " + item
mutex.release()
ans = ans + " ;" + tool_comment(tno)
return ans.strip()
# 'p' interface command (ran or nonran toolchanger)
def user_put_tool(tno,params):
mutex.acquire() # blocking
check_params(tno,params)
update_tool_params(tno,params.upper() )
save_tools_to_file(db_savefile,"tno:%3d updated (user_put_tool)"%tno)
mutex.release()
# 'l' interface command (nonran toolchanger)
def user_load_spindle_nonran_tc(tno,params):
mutex.acquire() # blocking
global spindle_tool
#msg("user_load_spindle_nonran_tc 'l %s'"%params)
check_params(tno,params)
if tno == spindle_tool:
# a tool reload may be requested (gui, worker task, etc.) using:
# emcmodule.cc:load_tool_table()
# --> EMC_TOOL_LOAD_TOOL_TABLE
# --> ioControl.cc:reload_tool_number()
# Note: the redundant reload is done for nonrandom toolchanger only
#msg("user_load_spindle_nonran_tc: redundant load for tno=%d"%tno)
mutex.release()
return
TMP = toolline_to_dict(params,['T','P'])
if TMP['P'] != "0": umsg("user_load_spindle_nonran_tc P=%s"%TMP['P'])
if tno == 0: umsg("user_load_spindle_nonran_tc tno=%d"%tno)
D = toolline_to_dict(tools[tno],all_letters)
# save nonran_from_pocket as pocket may have been altered by apply_db_rules()
nonran_from_pocket[tno] = D['P']
if spindle_tool > 0 and spindle_tool != tno: # accrue time for prior tool:
stop_tool_timer(spindle_tool)
nonran_restore_pocket(spindle_tool)
spindle_tool = tno
D['T'] = str(tno)
D['P'] = "0"
start_tool_timer(spindle_tool)
tools[tno] = dict_to_toolline(D,all_letters)
save_tools_to_file(db_savefile,"tno:%3d ( load to spindle)"%tno)
mutex.release()
# 'u' interface command (nonran toolchanger)
def user_unload_spindle_nonran_tc(tno,params):
mutex.acquire() # blocking
global spindle_tool
#msg("user_unload_spindle_nonran_tc spindle_tool=%d restore=%s'u %s'"%
# (spindle_tool,nonran_from_pocket,params))
check_params(tno,params)
if spindle_tool == -1: # no tool in spindle
pass # ignore
else:
TMP = toolline_to_dict(params,['T','P'])
if tno != 0: umsg("user_unload_spindle_nonran_tc tno=%d"%tno)
if TMP['P'] != "0": umsg("user_unload_spindle_nonran_tc P=%s"%TMP['P'])
stop_tool_timer(spindle_tool)
nonran_restore_pocket(spindle_tool)
spindle_tool = -1 # indicate no tool in spindle
save_tools_to_file(db_savefile,"tno:%3d (unload from spindle)(empty)"%tno)
mutex.release()
# 'l' interface command (ran toolchanger)
def user_load_spindle_ran_tc(tno,params):
mutex.acquire() # blocking
global spindle_tool
#msg("user_load_spindle_ran_tc 'l %s' (spindle_tool=%d)"%(params,spindle_tool))
check_params(tno,params)
D = toolline_to_dict(tools[tno],all_letters)
TMP = toolline_to_dict(params,['T','P'])
D['T'] = TMP['T']
D['P'] = TMP['P']
if spindle_tool != -1: # unexpected: no tool in spindle
stop_tool_timer(spindle_tool)
umsg("user_load_spindle_ran_tc spindle_tool=%d"%spindle_tool)
spindle_tool = tno
if tno != 0: start_tool_timer(spindle_tool)
tools[tno] = dict_to_toolline(D,all_letters)
pno = int(D['P'])
txt = "tno:%3d --> pno:%3d ( load to spindle)"%(tno,pno)
if tno==0 and pno==0:
txt = "tno:%3d --> pno:%3d ( load t0 spindle)(empty)"%(tno,pno)
save_tools_to_file(db_savefile,txt)
mutex.release()
# 'u' interface command (ran toolchanger)
def user_unload_spindle_ran_tc(tno,params):
mutex.acquire() # blocking
global spindle_tool
#msg("user_unload_spindle_ran_tc 'u %s' (spindle_tool=%d)"%(params,spindle_tool))
check_params(tno,params)
TMP = toolline_to_dict(params,['T','P'])
if spindle_tool == -1: # no tool in spindle
if tno != 0:
umsg("user_unload_spindle_ran_tc spindle_tool=%d tno=%d"%(spindle_tool,tno))
D = dict()
D['T'] = TMP['T']
D['P'] = TMP['P']
tools[0] = dict_to_toolline(D,['T','P'])
else:
if spindle_tool != 0: stop_tool_timer(spindle_tool)
D = toolline_to_dict(tools[spindle_tool],all_letters)
D['T'] = TMP['T']
D['P'] = TMP['P']
tools[tno] = dict_to_toolline(D,all_letters)
spindle_tool = -1 # indicate no tool in spindle
pno = int(D['P'])
txt="tno:%3d --> pno:%3d (unload from spindle)"%(tno,pno)
if tno==0:
txt="tno:%3d --> pno:%3d (unload from spindle)(notool)"%(tno,pno)
save_tools_to_file(db_savefile,txt)
mutex.release()
#-----------------------------------------------------------
start_db_program() # handle random/nonrandom toolchanger, args
# begin interface to LinuxCNC
debug = 0 # debug var for command line testing
if (len(sys.argv) > 1 and sys.argv[1] == 'debug'): debug = 1
toollist = make_tools(toolno_min,toolno_max)
if random_toolchanger: tooldb_callbacks(user_get_tool,
user_put_tool,
user_load_spindle_ran_tc,
user_unload_spindle_ran_tc)
else: tooldb_callbacks(user_get_tool,
user_put_tool,
user_load_spindle_nonran_tc,
user_unload_spindle_nonran_tc)
tooldb_tools(toollist)
#-----------------------------------------------------------
# start task threads
active_threads=[]
def start_thread(tname):
name = tname.__name__
if name in active_threads:
umsg("start_thread: %s already running"%name)
return
active_threads.append(name)
msg("**Start: %s"%name)
msg("**Active threads:%s"%active_threads)
thd = threading.Thread(target=tname,args=(disconnect_evt,),daemon=0)
thd.start()
return thd
def cmd_loop(*args):
try:
tooldb_loop() # loop forever, use callbacks
except BrokenPipeError:
handle_disconnect(db_savefile)
except Exception as e:
if sys.stdin.isatty():
msg(("Exception=%s"%str(e)))
else: pass # avoid messages at termination
# start cmd_loop thread (primary interface to LinuxCNC):
threading.Thread(target=cmd_loop,args=(disconnect_evt,),daemon=1).start()
# task to demonstrate periodic updates to database
def periodic_task(fini):
if fini.is_set(): return # end task
global start_time, elapsed_time, spindle_tool
while 1:
time.sleep(period_minutes*60)
mutex.acquire() # blocking
if spindle_tool > 0: update_tool_time(spindle_tool)
mutex.release()
threading.Thread(target=periodic_task,args=(disconnect_evt,),daemon=1).start()
#-----------------------------------------------------------
# begin worker demo tasks
def demo_add_or_rm_dbtool(tno):
# remove tool if it is in toollist
# add tool if it is not in toollist
if tno in toollist:
D = toolline_to_dict(tools[tno],tletters)
pno = int(D['P'])
if rm_dbtool(tno):
release_pocket(pno)
return 1 # success
else:
# supply some dummy parameter values based on tno
if add_dbtool(tno,Z=tno/100,D=tno/1000): return 1 # success
return 0 # fail
# tool_update_task: demonstrate addition/removal of toolno.s
def tool_update_task(fini):
ct_max = 2
wait_secs = 5 # interval between adds/removes
begin_tno = 40 # start of extra tool numbers
tno = begin_tno; ct = 0; waited = 0 ; delta_secs = 1
msg("**Begin tool_update_task begin_tno=%d ct_max=%d wait_secs=%d"%
(begin_tno,ct_max,wait_secs))
while ct < ct_max: # loop adding/removing some tool no.s
if fini.is_set(): return # end task
time.sleep(delta_secs); waited += delta_secs
if waited < wait_secs: continue
if not mutex.acquire(timeout=1):
msg("tool_update_task ct=%d waiting for mutex"%ct)
continue
demo_add_or_rm_dbtool(tno)
mutex.release()
ct += 1; waited = 0
tno = begin_tno + ct%n_extra_pockets
active_threads.remove('tool_update_task')
msg("**End tool_update_task ct=%d"%ct)
# tool_modify_task: demonstrate changing a tool parameter
def tool_modify_task(fini):
ct_max = 2
wait_secs = 5 # interval between tool modifications
letter = 'A'
modify_tnos = [15,16]
if not hasattr(tool_modify_task,"ct"): tool_modify_task.ct=0
tno = modify_tnos[tool_modify_task.ct]
tool_modify_task.ct = (tool_modify_task.ct +1)%len(modify_tnos);
ct = 0; waited = 0 ; delta_secs = 1
msg("**Begin tool_modify_task tno=%d letter=%s ct_max=%d wait_secs=%d"%
(tno,letter,ct_max,wait_secs))
while ct < ct_max: # loop adding/removing some tool no.s
if fini.is_set(): return # end task
time.sleep(delta_secs); waited += delta_secs
if waited < wait_secs: continue
waited = 0
if not sync_allowed():
msg("tool_modify_task: DISALLOWED tno=%d (%s)"%(tno,sync_fail_reason))
continue
if not mutex.acquire(timeout=1):
msg("tool_modify_task ct=%d waiting for mutex"%ct)
continue
D = toolline_to_dict(tools[tno],all_letters)
if spindle_tool != tno:
D[letter] = "0.1%02d"%ct # modify letter parameter
tools[tno] = dict_to_toolline(D,all_letters)
save_tools_to_file(db_savefile,"tno:%3d modified param %s=%s"%(tno,letter,D[letter]))
msg("tool_modify_task: tno=%d %s=%s"%(tno,letter,D[letter]));
linuxcnc_tool_sync()
ct += 1
else:
msg("tool_modify_task: DISALLOWED tno=%d (%s)"%(tno,"tool is loaded"))
mutex.release()
active_threads.remove('tool_modify_task')
msg("**End tool_modify_task ct=%d"%ct)
#-----------------------------------------------------------
# start worker tasks
time.sleep(1) # wait for startup messages from LinuxCNC and gui to pass
if cmdline_only: # for gui, start worker threads:
msg("cmdline debug (CTRL-C to exit)")
else:
msg("\n\nNOTE: Terminal command to monitor database file:\n $ watch -c -n 1 cat %s\n"%db_savefile)
thd1 = start_thread(tool_modify_task)
thd1.join() # comment this to make tasks concurrent
thd2 = start_thread(tool_update_task)
#-----------------------------------------------------------
# After all worker threads started, allow signals to start tasks.
# typ: kill -USR[12] $(pidof -x db_nonran.py)
def start_m_thread(*args):start_thread(tool_modify_task)
def start_u_thread(*args):start_thread(tool_update_task)
signal.signal(signal.SIGUSR1, start_m_thread)
signal.signal(signal.SIGUSR2, start_u_thread)
#-----------------------------------------------------------
disconnect_evt.wait()
msg("bye")