470 lines
20 KiB
Python
Executable File
470 lines
20 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
'''
|
|
migrate.py
|
|
'''
|
|
|
|
import sys
|
|
import os
|
|
from shutil import *
|
|
from tkinter import *
|
|
from tkinter import filedialog
|
|
import configparser
|
|
import gettext
|
|
|
|
if os.path.dirname(__file__).startswith('/usr/share'):
|
|
localeDir = '/usr/share/locale'
|
|
INSTYPE = ['pkg', 'linuxcnc']
|
|
HALLIB = '/usr/share/linuxcnc/hallib'
|
|
else:
|
|
path = os.path.dirname(__file__).split('configs')[0]
|
|
localeDir = os.path.join(path, 'share/locale')
|
|
INSTYPE = ['rip', os.path.join(path, 'scripts/linuxcnc')]
|
|
HALLIB = os.path.join(path, 'lib/hallib')
|
|
|
|
iconPath = os.path.join(os.path.dirname(__file__), 'lib/images/chips_plasma.png')
|
|
|
|
gettext.install("linuxcnc", localedir=localeDir)
|
|
_ = gettext.gettext
|
|
|
|
class Dialog(Toplevel):
|
|
def __init__(self, parent, title, text, buttons=1, insType=False, entry=None):
|
|
super().__init__()
|
|
# self.transient(parent)
|
|
self.grab_set()
|
|
self.attributes('-type', 'popup_menu')
|
|
self.overrideredirect(True)
|
|
self.resizable(False, False)
|
|
self.reply = False
|
|
self.value = StringVar()
|
|
self.entry = entry
|
|
self.parent = parent
|
|
self.focus_set()
|
|
justify = 'left' if 'Migration Log' in title else 'center'
|
|
f = Frame(self, relief='ridge', borderwidth=4)
|
|
f.grid(row=0, column=1, padx=1, pady=1)
|
|
t = Label(f, text=title)
|
|
t.grid(row=0, column=0, columnspan=3, sticky='ew')
|
|
l = Label(f, text=f"{text}\n", justify=justify)
|
|
l.grid(row=1, column=0, columnspan=3, padx=8, pady=(8,0))
|
|
if entry:
|
|
e = Entry(f, textvariable=self.value, justify='center')
|
|
e.grid(row=2, column=0, columnspan=3, sticky='ew', padx=8, pady=(0,8))
|
|
e.insert(END, entry)
|
|
e.icursor(END)
|
|
e.focus_set()
|
|
if insType:
|
|
text = 'Copy Command'
|
|
key = '<c>'
|
|
else:
|
|
text = 'Cancel'
|
|
key = '<Key-Escape>'
|
|
b1 = Button(f, text='OK', width=12, command=lambda:self.on_button(1))
|
|
self.bb1 = self.bind('<Key-Return>', lambda e:self.on_button(1))
|
|
if buttons == 3:
|
|
b2 = Button(f, text='View Log', width=12, command=lambda:self.on_button(2))
|
|
self.bb2 = self.bind('<v>', lambda e:self.on_button(2))
|
|
b2.grid(row=3, column=1, pady=(0,8))
|
|
if buttons > 1:
|
|
b0 = Button(f, text=text, width=12, command=lambda:self.on_button(0))
|
|
self.bb0 = self.bind(key, lambda e:self.on_button(0))
|
|
b1.grid(row=3, column=0, sticky='w', padx=(8,0), pady=(0,8))
|
|
b0.grid(row=3, column=2, sticky='e', padx=(0,8), pady=(0,8))
|
|
else:
|
|
b1.grid(row=3, column=1, padx=(0,8), pady=(0,8))
|
|
parent.eval(f"tk::PlaceWindow {self} center")
|
|
|
|
def on_button(self, button):
|
|
if button == 2:
|
|
title = 'plasmac2 Migration Log'
|
|
text = ''
|
|
with open(self.parent.logFile, 'r') as inFile:
|
|
for line in inFile:
|
|
text += line
|
|
Dialog(self.parent, title, text).show()
|
|
self.reply = 'log'
|
|
elif button and self.entry:
|
|
self.reply = self.value.get()
|
|
else:
|
|
self.reply = button
|
|
self.parent.focus_set()
|
|
self.destroy()
|
|
|
|
def show(self):
|
|
self.wm_deiconify()
|
|
self.wait_window()
|
|
return self.reply
|
|
|
|
class Migrate(Tk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
icon = PhotoImage(file=iconPath)
|
|
self.eval(f"wm iconphoto . {icon}")
|
|
self.title('plasmac2 Migrate')
|
|
text = _('Migration utility for plasmac2')
|
|
l = Label(self, text=text)
|
|
self.label = Label(self, justify='left')
|
|
self.migrate_text()
|
|
b = Frame(self)
|
|
bm = Button(b, text=_('Migrate'), width=10, command=self.migrate, padx=0)
|
|
bq = Button(b, text=_('Quit'), width=10, command=self.shutdown, padx=0)
|
|
bm.grid(row=0, column=0)
|
|
bq.grid(row=0, column=2)
|
|
l.grid(row=0, column=0, sticky='ew', padx=8, pady=(8,0))
|
|
self.label.grid(row=1, column=0, sticky='ew', padx=8, pady=(8,0))
|
|
b.grid(row=3, column=0, sticky='ew', padx=8, pady=(16,8))
|
|
b.columnconfigure((0,1,2), weight=1)
|
|
self.lcnc = os.path.expanduser('~/linuxcnc')
|
|
if not os.path.isdir(self.lcnc):
|
|
title = _('Directory Error')
|
|
msg1 = _('The directory ~/linuxcnc does not exist')
|
|
msg2 = _('It needs to have been created by LinuxCNC')
|
|
msg3 = _('and have a working QtPlasmaC configuration')
|
|
msg4 = _('to ensure that the structure is correct')
|
|
Dialog(self, title, f"{msg1}.\n{msg2}\n{msg3}\n{msg4}.\n").show()
|
|
raise SystemExit
|
|
if 'root' in self.lcnc:
|
|
title = _('User Error')
|
|
msg = _('plasmac2 setup can not be run as a root user')
|
|
Dialog(self, title, f"{msg}.\n").show()
|
|
raise SystemExit
|
|
self.configDir = os.path.join(self.lcnc, 'configs')
|
|
self.currentDir = os.path.dirname(__file__)
|
|
self.prefs = configparser.ConfigParser()
|
|
self.prefs.optionxform=str
|
|
self.eval(f"tk::PlaceWindow {self} center")
|
|
self.bind('<m>', lambda e:self.migrate())
|
|
self.bind('<q>', lambda e:self.shutdown())
|
|
|
|
def migrate(self):
|
|
self.migrate_text()
|
|
ini = filedialog.askopenfilename(
|
|
title='QtPlasmaC INI File',
|
|
initialdir=self.configDir,
|
|
filetypes=(('ini files', '*.ini'),))
|
|
if not ini:
|
|
return
|
|
iniFile = os.path.basename(ini)
|
|
self.oldDir = os.path.dirname(ini)
|
|
path, base = self.oldDir.rsplit('/',1)
|
|
newBase = f"{base}_plasmac2"
|
|
title = _('Directory Name')
|
|
msg1 = _('The new directory name will be')
|
|
msg2 = _('If you change it to the same name as the QtPlasmaC config')
|
|
msg3 = _('then the QtPlasmaC config directory will be renamed to:')
|
|
msg4 = _('Accept it or change it then accept')
|
|
reply = Dialog(self, title, f"{msg1}: {newBase}\n\n{msg2}\n{msg3}\n{base}_qtplasmac\n\n{msg4}\n", buttons=2, entry=newBase).show()
|
|
if reply == base:
|
|
try:
|
|
orgDir = self.oldDir
|
|
self.oldDir = f"{self.oldDir}_qtplasmac"
|
|
os.rename(orgDir, self.oldDir)
|
|
except Exception as e:
|
|
title = 'Directory Error'
|
|
msg1 = _('Error while renaming')
|
|
Dialog(self, title, f"{msg1}: {orgDir}\n\nto: {self.oldDir}\n\n{e}").show()
|
|
return
|
|
elif not reply:
|
|
return
|
|
self.newDir = os.path.join(path, reply)
|
|
self.newIni = os.path.join(self.newDir, iniFile)
|
|
self.logFile = os.path.join(self.newDir, 'migrate.log')
|
|
self.log = []
|
|
if os.path.isfile(self.logFile):
|
|
os.remove(self.logFile)
|
|
if os.path.exists(self.newDir):
|
|
title = _('Directory Exists')
|
|
msg1 = _('already exists')
|
|
msg2 = _('Overwrite')
|
|
reply = Dialog(self, title, f"{self.newDir} {msg1}\n\n{msg2}?\n", buttons=2).show()
|
|
if not reply:
|
|
return
|
|
if os.path.isfile(self.newDir) or os.path.islink(self.newDir):
|
|
os.remove(self.newDir)
|
|
elif os.path.isdir(self.newDir):
|
|
rmtree(self.newDir)
|
|
try:
|
|
ignores = ignore_patterns('qtplasmac','backups','machine_log*.txt', \
|
|
'*.qss','*.py','*.bak','qtvcp.prefs','M190', \
|
|
'qtplasmac.prefs')
|
|
# copy required files
|
|
p2Path = os.path.join(self.newDir, 'plasmac2')
|
|
copytree(self.oldDir, self.newDir, ignore=ignores)
|
|
# remove any existing plasmac2
|
|
if os.path.islink(p2Path):
|
|
os.remove(p2Path)
|
|
elif os.path.exists(p2Path):
|
|
os.remove(p2Path)
|
|
# create a link to plasmac2
|
|
os.symlink(self.currentDir, p2Path)
|
|
# copy user custom files
|
|
self.copy_custom_files(self.newDir)
|
|
self.do_ini_file()
|
|
self.do_hal_files()
|
|
self.do_prefs_file()
|
|
# all done...
|
|
path = os.path.join(self.newDir, iniFile)
|
|
title = _('Migration Complete')
|
|
msg1 = _('The INI file for the plasmac2 config is')
|
|
msg2 = _('Copy Command will copy the complete command required to run LinuxCNC using this config')
|
|
msg = f"{msg1}:\n\n{path}\n\n{msg2}.\n"
|
|
clip = False
|
|
btns = 2
|
|
if self.log:
|
|
with open(self.logFile, 'w') as outFile:
|
|
for line in self.log:
|
|
outFile.write(line)
|
|
msg1 = '-------------------------------------------------'
|
|
msg2 = _('Some hal files contain references to qtplasmac and they were commented out in the file')
|
|
msg3 = _('There is a log of these files in')
|
|
msg4 = _('in the new config directory')
|
|
msg += f"\n{msg1}\n\n{msg2}.\n\n{msg3} {os.path.basename(self.logFile)} {msg4}.\n"
|
|
btns = 3
|
|
while 1:
|
|
reply = Dialog(self, title, msg, buttons=btns, insType=INSTYPE[0]).show()
|
|
if reply == 1:
|
|
if clip:
|
|
text1 = _('Warning')
|
|
text2 = _('The copied command will be removed from the')
|
|
text3 = _('system clipboard when this window is closed')
|
|
self.label['text'] = f"{text1}:\n{text2}\n{text3}.\n"
|
|
break
|
|
elif reply == 0:
|
|
clip = True
|
|
self.clipboard_clear()
|
|
self.clipboard_append(f"{INSTYPE[1]} {path}")
|
|
self.update()
|
|
except Exception as e:
|
|
title = _('Migration error')
|
|
line = sys.exc_info()[-1].tb_lineno
|
|
msg1 = _('Migration was unsuccessful')
|
|
msg2 = _('Error in line')
|
|
Dialog(self, title, f"{msg1}.\n\n{msg2}: {line}\n{str(e)}").show()
|
|
|
|
def do_ini_file(self):
|
|
with open(self.newIni, 'r') as inFile:
|
|
config = inFile.readlines()
|
|
# [DISPLAY] section
|
|
section = {}
|
|
for lNum in range(config.index('[DISPLAY]\n') + 1, len(config)):
|
|
if config[lNum].startswith('['):
|
|
break
|
|
section[lNum] = config[lNum]
|
|
insert = lNum - 1 if config[lNum - 1] == '\n' else lNum
|
|
done = []
|
|
for lNum in section:
|
|
if section[lNum].startswith('DISPLAY') and 'd' not in done:
|
|
config[lNum] = 'DISPLAY = axis\n'
|
|
done.append('d')
|
|
elif section[lNum].startswith('OPEN_FILE') and 'of' not in done:
|
|
config[lNum] = 'OPEN_FILE = ""\n'
|
|
done.append('of')
|
|
elif section[lNum].startswith('TOOL_EDITOR') and 'te' not in done:
|
|
config[lNum] = 'TOOL_EDITOR = tooledit x y\n'
|
|
done.append('te')
|
|
elif section[lNum].startswith('CYCLE_TIME') and 'ct' not in done:
|
|
config[lNum] = 'CYCLE_TIME = 100\n'
|
|
done.append('ct')
|
|
elif section[lNum].startswith('USER_COMMAND_FILE') and 'uc' not in done:
|
|
config[lNum] = 'USER_COMMAND_FILE = ./plasmac2/plasmac2.py\n'
|
|
done.append('uc')
|
|
elif section[lNum].startswith('POSITION_OFFSET') and 'po' not in done:
|
|
config[lNum] = 'POSITION_OFFSET = RELATIVE\n'
|
|
done.append('po')
|
|
elif section[lNum].startswith('POSITION_FEEDBACK') and 'pf' not in done:
|
|
config[lNum] = 'POSITION_FEEDBACK = ACTUAL\n'
|
|
done.append('pf')
|
|
for option in ['d','of','te','ct','uc','po','pf']:
|
|
if option == 'd' and option not in done:
|
|
config.insert(insert,'DISPLAY = axis\n')
|
|
elif option == 'of' and option not in done:
|
|
config.insert(insert,'OPEN_FILE = ""\n')
|
|
elif option == 'te' and option not in done:
|
|
config.insert(insert,'TOOL_EDITOR = tooledit x y\n')
|
|
elif option == 'ct' and option not in done:
|
|
config.insert(insert,'CYCLE_TIME = 100\n')
|
|
elif option == 'uc' and option not in done:
|
|
config.insert(insert,'USER_COMMAND_FILE = ./plasmac2/plasmac2.py\n')
|
|
elif option == 'po' and option not in done:
|
|
config.insert(insert,'POSITION_OFFSET = RELATIVE\n')
|
|
elif option == 'pf' and option not in done:
|
|
config.insert(insert,'POSITION_FEEDBACK = ACTUAL\n')
|
|
# [RS274NGC] section
|
|
section = {}
|
|
for lNum in range(config.index('[RS274NGC]\n') + 1, len(config)):
|
|
if config[lNum].startswith('['):
|
|
break
|
|
section[lNum] = config[lNum]
|
|
insert = lNum - 1 if config[lNum - 1] == '\n' else lNum
|
|
done = []
|
|
for lNum in section:
|
|
if section[lNum].startswith('USER_M_PATH') and 'u' not in done:
|
|
mPath = config[lNum].split('=')[1].strip()
|
|
config[lNum] = f"USER_M_PATH = ./plasmac2:{mPath}\n"
|
|
done.append('u')
|
|
for option in ['u']:
|
|
if option == 'u' and option not in done:
|
|
config.insert(insert,'USER_M_PATH = ./plasmac2:./\n')
|
|
# [EMC] section - to get machine name
|
|
section = {}
|
|
for lNum in range(config.index('[EMC]\n') + 1, len(config)):
|
|
if config[lNum].startswith('['):
|
|
break
|
|
section[lNum] = config[lNum]
|
|
insert = lNum - 1 if config[lNum - 1] == '\n' else lNum
|
|
done = []
|
|
for lNum in section:
|
|
if section[lNum].startswith('MACHINE'):
|
|
self.machineName = config[lNum].split('=')[1].strip()
|
|
# [FILTER] section
|
|
section = {}
|
|
for lNum in range(config.index('[FILTER]\n') + 1, len(config)):
|
|
if config[lNum].startswith('['):
|
|
break
|
|
section[lNum] = config[lNum]
|
|
insert = lNum - 1 if config[lNum - 1] == '\n' else lNum
|
|
done = []
|
|
for lNum in section:
|
|
if section[lNum].startswith('PROGRAM_EXTENSION') and 'p' not in done:
|
|
config[lNum] = 'PROGRAM_EXTENSION = .ngc,.nc,.tap (filter gcode files)\n'
|
|
done.append('p')
|
|
if section[lNum].startswith('ngc') and 'g' not in done:
|
|
config[lNum] = 'ngc = qtplasmac_gcode\n'
|
|
done.append('g')
|
|
if section[lNum].startswith('nc') and 'c' not in done:
|
|
config[lNum] = 'nc = qtplasmac_gcode\n'
|
|
done.append('c')
|
|
if section[lNum].startswith('tap') and 'a' not in done:
|
|
config[lNum] = 'tap = qtplasmac_gcode\n'
|
|
done.append('a')
|
|
for option in ['a','c','g','p']:
|
|
if option == 'a' and option not in done:
|
|
config.insert(insert,'tap = qtplasmac_gcode\n')
|
|
elif option == 'c' and option not in done:
|
|
config.insert(insert,'nc = qtplasmac_gcode\n')
|
|
elif option == 'g' and option not in done:
|
|
config.insert(insert,'ngc = qtplasmac_gcode\n')
|
|
elif option == 'p' and option not in done:
|
|
config.insert(insert,'PROGRAM_EXTENSION = .ngc,.nc,.tap (filter gcode files)\n')
|
|
# [HAL] section
|
|
section = {}
|
|
self.halList = []
|
|
sim = False
|
|
self.qtplasmacHal = []
|
|
for lNum in range(config.index('[HAL]\n') + 1, len(config)):
|
|
if config[lNum].startswith('['):
|
|
break
|
|
section[lNum] = config[lNum]
|
|
for lNum in section:
|
|
if section[lNum].startswith('HALFILE') or \
|
|
section[lNum].startswith('POSTGUI_HALFILE') or \
|
|
section[lNum].startswith('SHUTDOWN'):
|
|
halFileRaw = config[lNum].split('=')[1].strip()
|
|
if os.path.isfile(os.path.join(HALLIB, halFileRaw)):
|
|
halFile = os.path.join(HALLIB, halFileRaw)
|
|
else:
|
|
halFile = os.path.realpath(os.path.join(self.oldDir, halFileRaw))
|
|
self.halList.append(halFileRaw)
|
|
# if existing config is a sim
|
|
if 'sim_postgui' in halFile:
|
|
sim = True
|
|
config[lNum] = ''
|
|
elif 'sim_no_stepgen.tcl' in halFile:
|
|
config[lNum] = 'HALFILE = plasmac2/lib/sim/sim_no_stepgen.tcl\n'
|
|
sim = True
|
|
elif 'sim_stepgen.tcl' in halFile:
|
|
config[lNum] = 'HALFILE = plasmac2/lib/sim/sim_stepgen.tcl\n'
|
|
sim = True
|
|
elif 'sim_' in halFile:
|
|
sim = True
|
|
# add sim panel if required
|
|
if sim:
|
|
app1 = '[APPLICATIONS]'
|
|
app2 = 'DELAY = 2'
|
|
app3 = 'APP = plasmac2/lib/sim/sim_panel.py'
|
|
config.insert(0, f"{app1}\n{app2}\n{app3}\n\n")
|
|
# write the ini file
|
|
with open(self.newIni, 'w') as outFile:
|
|
for line in config:
|
|
outFile.write(line)
|
|
|
|
def do_hal_files(self):
|
|
validPins = ['out_0', 'out_1', 'out_2', 'abort','frame-job','pause', \
|
|
'power','probe','pulse','run','run-pause','touchoff']
|
|
for file in self.halList:
|
|
if not os.path.dirname(file):
|
|
halFile = os.path.join(self.newDir, file)
|
|
edit = False
|
|
logged = False
|
|
with open(halFile, 'r') as inFile:
|
|
config = inFile.readlines()
|
|
for lNum in range(len(config)):
|
|
if 'qtplasmac.ext_' in config[lNum]:
|
|
for pin in validPins:
|
|
if f"qtplasmac.ext_{pin}" in config[lNum]:
|
|
config[lNum] = config[lNum].replace(f"qtplasmac.ext_{pin}", f"axisui.ext.{pin}")
|
|
edit = True
|
|
if 'qtplasmac' in config[lNum] or 'sim-torch' in config[lNum]:
|
|
config[lNum] = f"# MIGRATION: {config[lNum]}"
|
|
edit = True
|
|
if not logged:
|
|
self.log.append(f"\n{file}\n")
|
|
logged = True
|
|
self.log.append(f"{lNum+1} - {config[lNum]}")
|
|
# write the hal fil
|
|
if edit:
|
|
with open(halFile, 'w') as outFile:
|
|
for line in config:
|
|
outFile.write(line)
|
|
|
|
def do_prefs_file(self):
|
|
prefsFile = os.path.join(self.newDir, f"{self.machineName}.prefs")
|
|
with open(prefsFile, 'r') as inFile:
|
|
config = inFile.readlines()
|
|
# [BUTTONS] section
|
|
edit = False
|
|
section = {}
|
|
for lNum in range(config.index('[BUTTONS]\n') + 1, len(config)):
|
|
if config[lNum].startswith('['):
|
|
break
|
|
section[lNum] = config[lNum]
|
|
for lNum in section:
|
|
if 'Name =' in section[lNum]:
|
|
name, value = config[lNum].split('=')
|
|
value = value.replace('\\',' ').replace('&&', '& ')
|
|
config[lNum] = f"{name}={value}"
|
|
edit = True
|
|
if 'Code =' in section[lNum] and 'offsets-view' in section[lNum]:
|
|
config[lNum-1] = f"{config[lNum-1].split('=')[0]}= \n"
|
|
config[lNum] = f"{config[lNum].split('=')[0]}= \n"
|
|
self.log.append(f"\n{self.machineName}.prefs\n")
|
|
text1 = _('is not available in plasmac2')
|
|
text2 = _('it has been removed from the prefs file')
|
|
self.log.append(f"{lNum} & {lNum+1} - offsets-view {text1}, {text2}.\n")
|
|
edit = True
|
|
elif 'qtplasmac.ext_' in config[lNum]:
|
|
config[lNum] = config[lNum].replace('qtplasmac.ext_','axisui.ext.')
|
|
edit = True
|
|
# write the prefs file
|
|
if edit:
|
|
with open(prefsFile, 'w') as outFile:
|
|
for line in config:
|
|
outFile.write(line)
|
|
|
|
def copy_custom_files(self, dir):
|
|
for f in ['commands', 'hal', 'periodic']:
|
|
src = os.path.join(self.currentDir, 'lib/custom', f"user_{f}.py")
|
|
copy(src, os.path.join(dir, '.'))
|
|
|
|
def migrate_text(self):
|
|
text1 = _('Makes a copy of an existing working QtPlasmaC config')
|
|
text2 = _('Then converts that copy to a working plasmac2 config')
|
|
self.label['text'] = f"{text1}.\n\n{text2}.\n"
|
|
|
|
def shutdown(self):
|
|
raise SystemExit
|
|
|
|
if __name__ == "__main__":
|
|
migrate = Migrate()
|
|
migrate.mainloop()
|