offsetpage_widget.py
1 #!/usr/bin/env python 2 # GladeVcp Widget - offsetpage 3 # 4 # Copyright (c) 2013 Chris Morley 5 # 6 # This program is free software: you can redistribute it and/or modify 7 # it under the terms of the GNU General Public License as published by 8 # the Free Software Foundation, either version 2 of the License, or 9 # (at your option) any later version. 10 # 11 # This program is distributed in the hope that it will be useful, 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 # GNU General Public License for more details. 15 16 # This widget reads the offsets for current tool, current G5x, G92 using python library linuxcnc. 17 # all other offsets are read directly from the Var file, if available, as linuxcnc does not give 18 # access to all offsets thru NML, only current ones. 19 # you can hide any axes or any columns 20 # set metric or imperial 21 # set the var file to search 22 # set the text formatting for metric/imperial separately 23 24 import sys, os, pango, linuxcnc 25 from hal_glib import GStat 26 datadir = os.path.abspath(os.path.dirname(__file__)) 27 AXISLIST = ['offset', 'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W', 'name'] 28 # we need to know if linuxcnc isn't running when using the GLADE editor 29 # as it causes big delays in response 30 lncnc_running = False 31 try: 32 import gobject, gtk 33 except: 34 print('GTK not available') 35 sys.exit(1) 36 37 # localization 38 import locale 39 BASE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..")) 40 LOCALEDIR = os.path.join(BASE, "share", "locale") 41 locale.setlocale(locale.LC_ALL, '') 42 43 # we put this in a try so there is no error in the glade editor 44 # linuxcnc is probably not running then 45 try: 46 INIPATH = os.environ['INI_FILE_NAME'] 47 except: 48 pass 49 50 class OffsetPage(gtk.VBox): 51 __gtype_name__ = 'OffsetPage' 52 __gproperties__ = { 53 'display_units_mm' : (gobject.TYPE_BOOLEAN, 'Display Units', 'Display in metric or not', 54 False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 55 'mm_text_template' : (gobject.TYPE_STRING, 'Text template for Metric Units', 56 'Text template to display. Python formatting may be used for one variable', 57 "%10.3f", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 58 'imperial_text_template' : (gobject.TYPE_STRING, 'Text template for Imperial Units', 59 'Text template to display. Python formatting may be used for one variable', 60 "%9.4f", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 61 'font' : (gobject.TYPE_STRING, 'Pango Font', 'Display font to use', 62 "sans 12", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 63 'highlight_color' : (gtk.gdk.Color.__gtype__, 'Highlight color', "", 64 gobject.PARAM_READWRITE), 65 'foreground_color' : (gtk.gdk.Color.__gtype__, 'Active text color', "", 66 gobject.PARAM_READWRITE), 67 'hide_columns' : (gobject.TYPE_STRING, 'Hidden Columns', 'A no-spaces list of axes to hide: xyzabcuvw and t are the options', 68 "", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 69 'hide_rows' : (gobject.TYPE_STRING, 'Hidden Rows', 'A no-spaces list of rows to hide: 0123456789abc are the options' , 70 "", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 71 } 72 __gproperties = __gproperties__ 73 74 __gsignals__ = { 75 'selection_changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_STRING,)), 76 } 77 78 79 def __init__(self, filename = None, *a, **kw): 80 super(OffsetPage, self).__init__() 81 self.gstat = GStat() 82 self.filename = filename 83 self.linuxcnc = linuxcnc 84 self.status = linuxcnc.stat() 85 self.cmd = linuxcnc.command() 86 self.hash_check = None 87 self.display_units_mm = 0 # imperial 88 self.machine_units_mm = 0 # imperial 89 self.program_units = 0 # imperial 90 self.display_follows_program = False # display units are chosen indepenadently of G20/G21 91 self.font = "sans 12" 92 self.editing_mode = False 93 self.highlight_color = gtk.gdk.Color("lightblue") 94 self.foreground_color = gtk.gdk.Color("red") 95 self.unselectable_color = gtk.gdk.Color("lightgray") 96 self.hidejointslist = [] 97 self.hidecollist = [] 98 self.wTree = gtk.Builder() 99 self.wTree.set_translation_domain("linuxcnc") # for locale translations 100 self.wTree.add_from_file(os.path.join(datadir, "offsetpage.glade")) 101 self.current_system = None 102 self.selection_mask = () 103 self.axisletters = ["x", "y", "z", "a", "b", "c", "u", "v", "w"] 104 105 # global references 106 self.store = self.wTree.get_object("liststore2") 107 self.all_window = self.wTree.get_object("all_window") 108 self.view2 = self.wTree.get_object("treeview2") 109 self.view2.connect('button_press_event', self.on_treeview2_button_press_event) 110 self.selection = self.view2.get_selection() 111 self.selection.set_mode(gtk.SELECTION_SINGLE) 112 self.selection.connect("changed", self.on_selection_changed) 113 self.modelfilter = self.wTree.get_object("modelfilter") 114 self.edit_button = self.wTree.get_object("edit_button") 115 self.edit_button.connect('toggled', self.set_editing) 116 zero_g92_button = self.wTree.get_object("zero_g92_button") 117 zero_g92_button.connect('clicked', self.zero_g92) 118 zero_rot_button = self.wTree.get_object("zero_rot_button") 119 zero_rot_button.connect('clicked', self.zero_rot) 120 self.set_font(self.font) 121 self.modelfilter.set_visible_column(10) 122 self.buttonbox = self.wTree.get_object("buttonbox") 123 for col, name in enumerate(AXISLIST): 124 if col > 9:break 125 temp = self.wTree.get_object("cell_%s" % name) 126 temp.connect('edited', self.col_editted, col) 127 temp = self.wTree.get_object("cell_name") 128 temp.connect('edited', self.col_editted, 10) 129 # reparent offsetpage box from Glades top level window to widgets VBox 130 window = self.wTree.get_object("offsetpage_box") 131 window.reparent(self) 132 133 # check the ini file if UNITS are set to mm 134 # first check the global settings 135 # if not available then the X axis units 136 try: 137 self.inifile = self.linuxcnc.ini(INIPATH) 138 units = self.inifile.find("TRAJ", "LINEAR_UNITS") 139 if units == None: 140 units = self.inifile.find("AXIS_X", "UNITS") 141 except: 142 print _("**** Offsetpage widget ERROR: LINEAR_UNITS not found in INI's TRAJ section") 143 units = "inch" 144 145 # now setup the conversion array depending on the machine native units 146 if units == "mm" or units == "metric" or units == "1.0": 147 self.machine_units_mm = 1 148 self.conversion = [1.0 / 25.4] * 3 + [1] * 3 + [1.0 / 25.4] * 3 149 else: 150 self.machine_units_mm = 0 151 self.conversion = [25.4] * 3 + [1] * 3 + [25.4] * 3 152 153 # check linuxcnc status every half second 154 gobject.timeout_add(500, self.periodic_check) 155 156 # Reload the offsets into display 157 def reload_offsets(self): 158 g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3 = self.read_file() 159 if g54 == None: return 160 # Get the offsets arrays and convert the units if the display 161 # is not in machine native units 162 g5x = self.status.g5x_offset 163 tool = self.status.tool_offset 164 g92 = self.status.g92_offset 165 rot = self.status.rotation_xy 166 167 if self.display_units_mm != self.machine_units_mm: 168 g5x = self.convert_units(g5x) 169 tool = self.convert_units(tool) 170 g92 = self.convert_units(g92) 171 g54 = self.convert_units(g54) 172 g55 = self.convert_units(g55) 173 g56 = self.convert_units(g56) 174 g57 = self.convert_units(g57) 175 g58 = self.convert_units(g58) 176 g59 = self.convert_units(g59) 177 g59_1 = self.convert_units(g59_1) 178 g59_2 = self.convert_units(g59_2) 179 g59_3 = self.convert_units(g59_3) 180 181 # set the text style based on unit type 182 if self.display_units_mm: 183 tmpl = self.mm_text_template 184 else: 185 tmpl = self.imperial_text_template 186 187 degree_tmpl = "%11.2f" 188 189 # fill each row of the liststore fron the offsets arrays 190 for row, i in enumerate([tool, g5x, rot, g92, g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3]): 191 for column in range(0, 9): 192 if row == 2: 193 if column == 2: 194 self.store[row][column + 1] = locale.format(degree_tmpl, rot) 195 else: 196 self.store[row][column + 1] = " " 197 else: 198 self.store[row][column + 1] = locale.format(tmpl, i[column]) 199 # set the current system's label's color - to make it stand out a bit 200 if self.store[row][0] == self.current_system: 201 self.store[row][13] = self.foreground_color 202 else: 203 self.store[row][13] = None 204 # mark unselectable rows a dirrerent color 205 if self.store[row][0] in self.selection_mask: 206 self.store[row][12] = self.unselectable_color 207 208 # This is for adding a filename path after the offsetpage is already loaded. 209 def set_filename(self, filename): 210 self.filename = filename 211 self.reload_offsets() 212 213 # We read the var file directly 214 # and pull out the info we need 215 # if anything goes wrong we set all the info to 0 216 def read_file(self): 217 try: 218 g54 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 219 g55 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 220 g56 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 221 g57 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 222 g58 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 223 g59 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 224 g59_1 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 225 g59_2 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 226 g59_3 = [0, 0, 0, 0, 0, 0, 0, 0, 0] 227 if self.filename == None: 228 return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3 229 if not os.path.exists(self.filename): 230 return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3 231 logfile = open(self.filename, "r").readlines() 232 for line in logfile: 233 temp = line.split() 234 param = int(temp[0]) 235 data = float(temp[1]) 236 237 if 5229 >= param >= 5221: 238 g54[param - 5221] = data 239 elif 5249 >= param >= 5241: 240 g55[param - 5241] = data 241 elif 5269 >= param >= 5261: 242 g56[param - 5261] = data 243 elif 5289 >= param >= 5281: 244 g57[param - 5281] = data 245 elif 5309 >= param >= 5301: 246 g58[param - 5301] = data 247 elif 5329 >= param >= 5321: 248 g59[param - 5321] = data 249 elif 5349 >= param >= 5341: 250 g59_1[param - 5341] = data 251 elif 5369 >= param >= 5361: 252 g59_2[param - 5361] = data 253 elif 5389 >= param >= 5381: 254 g59_3[param - 5381] = data 255 return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3 256 except: 257 return None, None, None, None, None, None, None, None, None 258 259 # This allows hiding or showing columns from a text string of columnns 260 # eg list ='ab' 261 # default, all the columns are shown 262 def set_col_visible(self, list, bool): 263 try: 264 for index in range(0, len(list)): 265 colstr = str(list[index]) 266 colnum = "xyzabcuvwt".index(colstr.lower()) 267 name = AXISLIST[colnum + 1] 268 renderer = self.wTree.get_object(name) 269 renderer.set_property('visible', bool) 270 except: 271 pass 272 273 # hide/show the offset rows from a text string of row ids 274 # eg list ='123' 275 def set_row_visible(self, list, bool): 276 try: 277 for index in range(0, len(list)): 278 rowstr = str(list[index]) 279 rownum = "0123456789abcd".index(rowstr.lower()) 280 self.store[rownum][10] = bool 281 except: 282 pass 283 284 # This does the units conversion 285 # it just multiplies the two arrays 286 def convert_units(self, v): 287 c = self.conversion 288 return map(lambda x, y: x * y, v, c) 289 290 # make the cells editable and highlight them 291 def set_editing(self, widget): 292 state = widget.get_active() 293 # stop updates from linuxcnc 294 self.editing_mode = state 295 # highlight editable rows 296 if state: 297 color = self.highlight_color 298 else: 299 color = None 300 # Set rows editable 301 for i in range(1, 13): 302 if not self.store[i][0] in('G5x', 'Rot', 'G92', 'G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1', 'G59.2', 'G59.3'): continue 303 if self.store[i][0] in self.selection_mask: continue 304 self.store[i][11] = state 305 self.store[i][12] = color 306 self.queue_draw() 307 308 # When the column is edited this does the work 309 # TODO the edited column does not end up showing the editted number even though linuxcnc 310 # registered the change 311 def col_editted(self, widget, filtered_path, new_text, col): 312 (store_path,) = self.modelfilter.convert_path_to_child_path(filtered_path) 313 row = store_path 314 axisnum = col - 1 315 # print "EDITED:", new_text, col, int(filtered_path), row, "axis num:", axisnum 316 317 def system_to_p(system): 318 convert = { "G54":1, "G55":2, "G56":3, "G57":4, "G58":5, "G59":6, "G59.1":7, "G59.2":8, "G59.3":9} 319 try: 320 pnum = convert[system] 321 except: 322 pnum = None 323 return pnum 324 325 # Hack to not edit any rotational offset but Z axis 326 if row == 2 and not col == 3: return 327 328 # set the text style based on unit type 329 if self.display_units_mm: 330 tmpl = lambda s: self.mm_text_template % s 331 else: 332 tmpl = lambda s: self.imperial_text_template % s 333 334 # allow 'name' columnn text to be arbitrarily changed 335 if col == 10: 336 self.store[row][14] = new_text 337 return 338 # set the text in the table 339 try: 340 self.store[row][col] = locale.format("%10.4f", locale.atof(new_text)) 341 except: 342 print _("offsetpage widget error: unrecognized float input") 343 # make sure we switch to correct units for machine and rotational, row 2, does not get converted 344 try: 345 if not self.display_units_mm == self.program_units and not row == 2: 346 if self.program_units == 1: 347 convert = 25.4 348 else: 349 convert = 1.0 / 25.4 350 qualified = float(locale.atof(new_text)) * convert 351 else: 352 qualified = float(locale.atof(new_text)) 353 except: 354 print 'error' 355 # now update linuxcnc to the change 356 try: 357 global lncnc_runnning 358 if lncnc_running: 359 if self.status.task_mode != self.linuxcnc.MODE_MDI: 360 self.cmd.mode(self.linuxcnc.MODE_MDI) 361 self.cmd.wait_complete() 362 if row == 1: 363 self.cmd.mdi("G10 L2 P0 %s %10.4f" % (self.axisletters[axisnum], qualified)) 364 elif row == 2: 365 if col == 3: 366 self.cmd.mdi("G10 L2 P0 R %10.4f" % (qualified)) 367 elif row == 3: 368 self.cmd.mdi("G92 %s %10.4f" % (self.axisletters[axisnum], qualified)) 369 else: 370 pnum = system_to_p(self.store[row][0]) 371 if not pnum == None: 372 self.cmd.mdi("G10 L2 P%d %s %10.4f" % (pnum, self.axisletters[axisnum], qualified)) 373 self.cmd.mode(self.linuxcnc.MODE_MANUAL) 374 self.cmd.wait_complete() 375 self.cmd.mode(self.linuxcnc.MODE_MDI) 376 self.cmd.wait_complete() 377 self.gstat.emit('reload-display') 378 except: 379 print _("offsetpage widget error: MDI call error") 380 self.reload_offsets() 381 382 383 # callback to cancel G92 when button pressed 384 def zero_g92(self, widget): 385 # print "zero g92" 386 if lncnc_running: 387 try: 388 if self.status.task_mode != self.linuxcnc.MODE_MDI: 389 self.cmd.mode(self.linuxcnc.MODE_MDI) 390 self.cmd.wait_complete() 391 self.cmd.mdi("G92.1") 392 self.cmd.mode(self.linuxcnc.MODE_MANUAL) 393 self.cmd.wait_complete() 394 self.cmd.mode(self.linuxcnc.MODE_MDI) 395 self.cmd.wait_complete() 396 self.gstat.emit('reload-display') 397 except: 398 print _("MDI error in offsetpage widget -zero G92") 399 400 # callback to zero rotational offset when button pressed 401 def zero_rot(self, widget): 402 # print "zero rotation offset" 403 if lncnc_running: 404 try: 405 if self.status.task_mode != self.linuxcnc.MODE_MDI: 406 self.cmd.mode(self.linuxcnc.MODE_MDI) 407 self.cmd.wait_complete() 408 self.cmd.mdi("G10 L2 P0 R 0") 409 self.cmd.mode(self.linuxcnc.MODE_MANUAL) 410 self.cmd.wait_complete() 411 self.cmd.mode(self.linuxcnc.MODE_MDI) 412 self.cmd.wait_complete() 413 self.gstat.emit('reload-display') 414 except: 415 print _("MDI error in offsetpage widget-zero rotational offset") 416 417 # check for linnuxcnc ON and IDLE which is the only safe time to edit the tool file. 418 # if in editing mode don't update else you can't actually edit 419 def periodic_check(self): 420 convert = ("None", "G54", "G55", "G56", "G57", "G58", "G59", "G59.1", "G59.2", "G59.3") 421 try: 422 self.status.poll() 423 on = self.status.task_state > linuxcnc.STATE_OFF 424 idle = self.status.interp_state == linuxcnc.INTERP_IDLE 425 self.edit_button.set_sensitive(bool(on and idle)) 426 self.current_system = convert[self.status.g5x_index] 427 self.program_units = int(self.status.program_units == 2) 428 if self.display_follows_program: 429 self.display_units_mm = self.program_units 430 global lncnc_running 431 lncnc_running = True 432 except: 433 self.current_system = "G54" 434 lncnc_running = False 435 436 if self.filename and not self.editing_mode: 437 self.reload_offsets() 438 return True 439 440 # sets the color when editing is active 441 def set_highlight_color(self, value): 442 self.highlight_color = gtk.gdk.Color(value) 443 444 # sets the text color of the current system description name 445 def set_foreground_color(self, value): 446 self.foreground_color = gtk.gdk.Color(value) 447 448 # Allows you to set the text font of all the rows and columns 449 def set_font(self, value): 450 for col, name in enumerate(AXISLIST): 451 if col > 10:break 452 temp = self.wTree.get_object("cell_" + name) 453 temp.set_property('font', value) 454 455 # helper function to set the units to inch 456 def set_to_inch(self): 457 self.display_units_mm = 0 458 459 # helper function to set the units to mm 460 def set_to_mm(self): 461 self.display_units_mm = 1 462 463 def set_display_follows_program_units(self): 464 self.display_follows_program = True 465 466 def set_display_independent_units(self): 467 self.display_follows_program = False 468 469 # helper function to hide control buttons 470 def hide_buttonbox(self, state): 471 if state: 472 self.buttonbox.hide() 473 else: 474 self.buttonbox.show() 475 476 # Mark the active system with cursor highlight 477 def mark_active(self, system): 478 try: 479 pathlist = [] 480 for row in self.modelfilter: 481 if row[0] == system: 482 pathlist.append(row.path) 483 if len(pathlist) == 1: 484 self.selection.select_path(pathlist[0]) 485 except: 486 print _("offsetpage_widget error: cannot select coordinate system"), system 487 488 # Get the selected row the user clicked 489 def get_selected(self): 490 model, iter = self.selection.get_selected() 491 if iter: 492 system = model.get_value(iter, 0) 493 name = model.get_value(iter, 14) 494 # print "System:%s Name:%s"% (system,name) 495 return system, name 496 else: 497 return None, None 498 499 def on_selection_changed(self, treeselection): 500 system, name = self.get_selected() 501 # print self.status.g5x_index 502 if system in self.selection_mask: 503 self.mark_active(self.current_system) 504 self.emit("selection_changed", system, name) 505 506 def set_names(self, names): 507 for offset, name in names: 508 for row in range(0, 13): 509 if offset == self.store[row][0]: 510 self.store[row][14] = name 511 512 def get_names(self): 513 temp = [] 514 for row in range(0, 13): 515 temp.append([self.store[row][0], self.store[row][14]]) 516 return temp 517 518 # For single click selection when in edit mode 519 def on_treeview2_button_press_event(self, widget, event): 520 if event.button == 1 : # left click 521 try: 522 path, model, x, y = widget.get_path_at_pos(int(event.x), int(event.y)) 523 self.view2.set_cursor(path, None, True) 524 except: 525 pass 526 527 # standard Gobject method 528 def do_get_property(self, property): 529 name = property.name.replace('-', '_') 530 if name in self.__gproperties.keys(): 531 return getattr(self, name) 532 else: 533 raise AttributeError('unknown property %s' % property.name) 534 535 # standard Gobject method 536 # This is so that in the Glade editor, you can change the display 537 def do_set_property(self, property, value): 538 name = property.name.replace('-', '_') 539 if name == 'font': 540 try: 541 self.set_font(value) 542 except: 543 pass 544 if name == 'hide_columns': 545 self.set_col_visible("xyzabcuvwt", True) 546 self.set_col_visible("%s" % value, False) 547 if name == 'hide_rows': 548 self.set_row_visible("0123456789abc", True) 549 self.set_row_visible("%s" % value, False) 550 if name in self.__gproperties.keys(): 551 setattr(self, name, value) 552 553 # boiler code for variable access 554 def __getitem__(self, item): 555 return getattr(self, item) 556 def __setitem__(self, item, value): 557 return setattr(self, item, value) 558 559 560 # for testing without glade editor: 561 # Must linuxcnc running to see anything 562 def main(filename = None): 563 window = gtk.Dialog("My dialog", 564 None, 565 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, 566 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 567 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) 568 offsetpage = OffsetPage() 569 570 window.vbox.add(offsetpage) 571 # offsetpage.set_filename("../../../configs/sim/gscreen_custom/sim.var") 572 # offsetpage.set_col_visible("Yabuvw", False) 573 # offsetpage.set_row_visible("456789abc", False) 574 # offsetpage.set_row_visible("89abc", True) 575 # offsetpage.set_to_mm() 576 # offsetpage.set_font("sans 20") 577 # offsetpage.set_property("highlight_color", gtk.gdk.Color('blue')) 578 # offsetpage.set_highlight_color("violet") 579 # offsetpage.set_foreground_color("yellow") 580 # offsetpage.mark_active("G55") 581 # offsetpage.selection_mask = ("Tool", "Rot", "G5x") 582 # offsetpage.set_names([['G54', 'Default'], ["G55", "Vice1"], ['Rot', 'Rotational']]) 583 # print offsetpage.get_names() 584 585 window.connect("destroy", gtk.main_quit) 586 window.show_all() 587 response = window.run() 588 if response == gtk.RESPONSE_ACCEPT: 589 print "True" 590 else: 591 print "False" 592 593 if __name__ == "__main__": 594 main() 595 596