#!/usr/bin/env python # -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (C) 2011, Paul Lutus * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU General Public License as published by * # * the Free Software Foundation; either version 2 of the License, or * # * (at your option) any later version. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU General Public License for more details. * # * * # * You should have received a copy of the GNU General Public License * # * along with this program; if not, write to the * # * Free Software Foundation, Inc., * # * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * # *************************************************************************** # version date 10-03-2011 VERSION = '1.5' # no serial support in python 3! import os, re, sys, signal import pango import serial import gtk import gobject import platform import webbrowser class Icon: icon = [ "32 32 17 1", " c None", ". c #000100", "+ c #1A1B19", "@ c #252827", "# c #424443", "$ c #4F504E", "% c #425F7F", "& c #4B6F97", "* c #6D6E6C", "= c #5B7FA7", "- c #818382", "; c #919391", "> c #7B96B7", ", c #AEB0AD", "' c #CBCDCB", ") c #E2E4E1", "! c #EFF1EE", " ", " ", " ........................... ", " .!!!!!!!!!!!!!!!!!))))))'',. ", " .!)!!!!!!!!!!!))))))))'''';. ", " .!)*....................*'-. ", " .!!.&&&&&&&&&&&&&&&&&&&%@'-. ", " .!).&>>>>>>>>>>>>>>>>>>=+'-. ", " .!).&>>>>>>>>>>>>>>>>>>=+'-. ", " .!).&>>>>>>>>>>>>>>>====+,-. ", " .!).&>>>>>>>>>>=========+,-. ", " .!).&>>>&&&&&&&&&%%%&&&=+,-. ", " .)).&>>&''''''''''''%&&=+,-. ", " .)'.&>>&''-))))))))'%&&=+,-. ", " .)'.&&&%''-))))))))'%&&=+,-. ", " .)'.&&&%'''''''''',.%&&>+,-.. ", " .)'.&&&&%%%%%%%%%%.'.%&>+,*.,.", " .''.&&&&&&&&&&&&&&&.!.&>+;.'. ", " .''.&==========&.==.!.&>+*.. ", " .''.&==========.'..!).%>..$. ", " .',.=>>>>>>>>>>>.'))),.#@$-. ", " .',$.................'$.#;-. ", " .',,,,,,,,,,,,,,,,,,,.@#.;-. ", " .,,,,,,,,,,,,,,,;;...#.'*.*. ", " .,;;;;;;;;;;;;;;;.-'>.;.'-.. ", " ...$$$$$$$$$$$$$.>'>>@##.!*. ", " +.*----;;;;;;-.;'>>%.***.'*. ", " ..............'>>%.......'$ ", " .>=%. .. ", " ... ", " ", " " ] # a convenience class for # communicating with gtk controls class ControlInterface: # pretend enumeration IS_BOOL,IS_COMBO,IS_STRING,IS_WINDOW,IS_UNKNOWN = list(range(5)) def __init__(self,obj,name): self.inst = obj self.name = name self.combo_array = None typ = type(obj) if(typ == gtk.RadioButton or typ == gtk.CheckButton): self.cat = ControlInterface.IS_BOOL elif(typ == gtk.ComboBox): # if no cell renderer if(len(obj.get_cells()) == 0): # Create a text cell renderer cell = gtk.CellRendererText () obj.pack_start(cell) obj.add_attribute (cell, "text", 0) self.cat = ControlInterface.IS_COMBO elif(typ == gtk.Entry): self.cat = ControlInterface.IS_STRING elif(typ == gtk.Window): self.cat = ControlInterface.IS_WINDOW else: self.cat = ControlInterface.IS_UNKNOWN def read(self): if(self.cat == ControlInterface.IS_BOOL): return self.inst.get_active() elif(self.cat == ControlInterface.IS_COMBO): return self.inst.get_active_text() elif(self.cat == ControlInterface.IS_STRING): return self.inst.get_text() elif(self.cat == ControlInterface.IS_WINDOW): return self.inst.get_size() else: return None def write(self,v): if(self.cat == ControlInterface.IS_BOOL): self.inst.set_active(str(v) == 'True') elif(self.cat == ControlInterface.IS_COMBO): if(v in self.combo_array): index = self.combo_array.index(v) self.inst.set_active(index) else: self.inst.set_active(0) elif(self.cat == ControlInterface.IS_STRING): self.inst.set_text(v) elif(self.cat == ControlInterface.IS_WINDOW): x,y = re.findall('\d+',v) self.inst.resize(int(x),int(y)) else: raise Exception("Trying to write data to unknown control type ", self.inst) def load_combo_list(self,ca): assert(self.cat == ControlInterface.IS_COMBO) # keep this list for later access self.combo_array = ca self.inst.get_model().clear() for s in ca: self.inst.append_text(s.strip()) class ConfigManager: def __init__(self,inst): self.parent = inst # config file located at (user home dir)/.classname self.configpath = os.path.expanduser("~/." + self.parent.__class__.__name__) self.read_widgets() # do this only once def read_widgets(self): # an unbelievable hack made necessary by # someone unwilling to fix a year-old bug with open(self.parent.xmlname) as f: data = f.read() array = re.findall(r'(?s) id="(.*?)"',data) for name in array: # only interested in names starting with 'k_' if re.search(r'^k_',name): obj = self.parent.builder.get_object(name) ci = ControlInterface(obj,name) # create parent reference with same name setattr(self.parent,name,ci) def read_config(self): if(os.path.exists(self.configpath)): with open(self.configpath) as f: for line in f.readlines(): name,value = re.search(r'^\s*(.*?)\s*=\s*(.*?)\s*$',line).groups() ci = getattr(self.parent,name,None) if(ci != None): ci.write(value) else: print("no object named %s" % name) def write_config(self): with open(self.configpath,'w') as f: for name in dir(self.parent): if re.search(r'^k_',name): ci = getattr(self.parent,name,None) if(ci != None and ci.read() != None): f.write("%s = %s\n" % (ci.name,ci.read())) class SerialTerminal: def __init__(self): self.builder = gtk.Builder() self.xmlname = "serialterminal_gui31.glade" self.builder.add_from_file(self.xmlname) self.operating_system = platform.system() self.white = gtk.gdk.color_parse("white") self.gray = gtk.gdk.color_parse("#e0e0e0") self.black = gtk.gdk.color_parse("black") self.CHAR_BELL = 7 self.CHAR_BS = 8 self.CHAR_LF = 10 self.CHAR_CR = 13 self.CHAR_TAB = 9 self.program_name = self.__class__.__name__ self.title = self.program_name + ' ' + VERSION self.logfn = self.program_name + ".log" self.loghandle = False self.running = False self.ser_handle = False self.old_satmode = False self.old_port = "" self.old_baud = "" self.ser_port_lock_path = "" self.keybuf = "" self.comline_history = [] self.comline_index = 0 self.comline_current = "" self.com_key_event = False self.main_key_event = False sat_data = ( "HALT", "@DEBUGON", "@SATCONFIG,C,54,12516,20000,99,0X1008,R,U,3", "@SATCONFIG,C,54,12516,20000,99,0X1008,R,U,3", "@SATCONFIG,C,54,12676,20000,99,0X1008,L,U,3", "@SATCONFIG,C,54,12676,20000,99,0X1008,L,U,3", "@SAVE,C", "@DEBUGOFF", "@SATCONFIG", ) self.sat_program = '|'.join(sat_data) baud_rates = ( "50", "75", "110", "134", "150", "200", "300", "600", "1200", "1800", "2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", ) self.keymap = { 'Return' : chr(self.CHAR_LF), 'BackSpace' : chr(self.CHAR_BS), 'Tab' : chr(self.CHAR_TAB), 'Up' : '\033[A', 'KP_Up' : '\033[A', 'Down' : '\033[B', 'KP_Down' : '\033[B', 'Right' : '\033[C', 'KP_Right' : '\033[C', 'Left' : '\033[D', 'KP_Left' : '\033[D', 'Home' : '\033[H', 'KP_Home' : '\033[H', 'End' : '\033[4~', 'KP_End' : '\033[4~', } self.config = ConfigManager(self) # now we have local names for gtk objects of interest self.k_size_combobox.load_combo_list([str(x) for x in range(4,65,2)]) self.k_baud_combobox.load_combo_list(baud_rates) if(self.operating_system == 'Windows'): devs = ['COM%d' % dev for dev in range(1,9)] else: devs = [dev for dev in sorted(os.listdir("/dev")) if (re.search('tty(U|S)',dev))] self.k_port_combobox.load_combo_list(devs) # initial must-have values self.k_baud_combobox.write('4800') self.k_port_combobox.write('ttyUSB0') self.k_size_combobox.write('10') # now read configuration file if exists self.config.read_config() self.mainwindow = self.k_ser_term.inst self.mainwindow.set_title(self.title) self.mainwindow.set_icon(gtk.gdk.pixbuf_new_from_xpm_data(Icon.icon)) self.term_window = self.k_term_window.inst self.tbuff = self.term_window.get_buffer() self.term_window.modify_bg(gtk.STATE_NORMAL,self.white) # gracefully exit on keyboard and other signals signal.signal(signal.SIGTERM, self.close) signal.signal(signal.SIGINT, self.close) # connect controls to actions self.connect_task() self.init_textbuffer() self.toggle_control() # set defaults self.init_serial() self.running = True # periodically read/write gobject.timeout_add(100,self.process_io) def connect_task(self): connect_list = ( (self.k_quit_button.inst,'clicked',self.close), (self.k_help_button.inst,'clicked',self.launch_help), (self.k_ser_term.inst,'unrealize',self.close), (self.k_scroll_checkbutton.inst,'toggled',self.toggle_control), (self.k_echo_checkbutton.inst,'toggled',self.toggle_control), (self.k_wrap_checkbutton.inst,'toggled',self.toggle_control), (self.k_log_checkbutton.inst,'toggled',self.toggle_control), (self.k_satmode_checkbutton.inst,'toggled',self.toggle_control), (self.k_latnbutton.inst,'toggled',self.toggle_control), (self.k_lngwbutton.inst,'toggled',self.toggle_control), (self.k_lat_entry.inst,'changed',self.toggle_control), (self.k_lng_entry.inst,'changed',self.toggle_control), (self.k_size_combobox.inst,'changed',self.toggle_control), (self.k_baud_combobox.inst,'changed',self.init_serial), (self.k_port_combobox.inst,'changed',self.init_serial), (self.k_clear_log_button.inst,'clicked',self.clear_log), (self.k_ser_term.inst,'focus-in-event', \ lambda w,e: self.focus_control(True)), (self.k_ser_term.inst,'focus-out-event', \ lambda w,e: self.focus_control(False)), # trap chars headed for text display (self.k_term_window.inst,'key-press-event', lambda w,e: True), (self.k_e119button.inst,'clicked',lambda w: self.com_string("@L,A")), (self.k_e110button.inst,'clicked',lambda w: self.com_string("@L,B")), (self.k_e129button.inst,'clicked',lambda w: self.com_string("@L,C")), (self.k_sleeponbutton.inst,'clicked', \ lambda w: self.com_string("DEBUGON|SLEEPON|DEBUGOFF")), (self.k_sleepoffbutton.inst,'clicked', \ lambda w: self.com_string("DEBUGON|SLEEPOFF|DEBUGOFF")), (self.k_program_button.inst,'clicked', \ lambda w: self.com_string(self.sat_program)), (self.k_list_button.inst,'clicked', \ lambda w: self.com_string("@SATCONFIG")), (self.k_setposbutton.inst,'clicked',self.send_pos_string), ) for tup in connect_list: tup[0].connect(tup[1],tup[2]) def focus_control(self,focus): col = ('gray','black')[focus] self.cursor_tag.set_property('background',col) def show_status(self,s): self.k_status_label.inst.set_text(s) def set_term_color(self,normal): col = (self.gray,self.white)[normal] self.term_window.modify_base(gtk.STATE_NORMAL,col) def launch_help(self,*args): webbrowser.open("http://arachnoid.com/python/serialterminal_program.html") def close(self,*args): self.running = False self.close_serial() self.config.write_config() self.control_log(False) gtk.main_quit() def toggle_control(self,*args): wm = gtk.WRAP_NONE if(self.k_wrap_checkbutton.read()): wm = gtk.WRAP_WORD_CHAR self.term_window.set_wrap_mode(wm) self.scroll_to_bottom() self.control_log(self.k_log_checkbutton.read()) fontsz = self.k_size_combobox.read() font = pango.FontDescription("Monospace,%s" % fontsz) self.term_window.modify_font(font) self.k_extension_vbox.inst.set_visible(self.k_satmode_checkbutton.read()) if(not self.running or self.old_satmode != self.k_satmode_checkbutton.read()): if(self.k_satmode_checkbutton.read()): # changing to sat mode if(self.main_key_event): self.k_ser_term.inst.disconnect(self.main_key_event) self.com_key_event = self.k_com_entry.inst.connect('key-press-event', \ self.com_keyboard_read) self.k_com_entry.inst.connect('key-release-event', lambda w,e: True) self.k_com_entry.inst.grab_focus() if(self.running): self.k_outcr_checkbutton.write(True) self.k_incr_checkbutton.write(False) self.k_baud_combobox.write('9600') self.init_serial() else: # changing to terminal mode if(self.com_key_event): self.k_com_entry.inst.disconnect(self.com_key_event) self.main_key_event = self.k_ser_term.inst.connect('key-press-event', \ self.main_keyboard_read) self.old_satmode = self.k_satmode_checkbutton.read() def send_pos_string(self,*args): pos = "GPS,%s,%s,%s,%s" % ( self.k_lat_entry.read(), ('S','N')[self.k_latnbutton.read()], self.k_lng_entry.read(), ('E','W')[self.k_lngwbutton.read()], ) s = "DEBUGON|%s|DEBUGOFF" % pos self.com_string(s) def clear_log(self,*args): oldstate = self.k_log_checkbutton.read() self.control_log(False) f = open(self.logfn,'w') f.close() self.control_log(oldstate) def control_log(self,state): self.k_log_checkbutton.write(state) if(self.k_log_checkbutton.read()): if not (self.loghandle): self.loghandle = open(self.logfn,'a') else: if(self.loghandle): self.loghandle.close() self.loghandle = False def write_log(self,s): if(self.loghandle): self.loghandle.write(s) def create_lock_path(self,port): return "/var/lock/lockdev/LCK..%s" % self.k_port_combobox.read() def serial_flag_control(self,state,port=""): flagpath = self.create_lock_path(port) if(state == 0): # check if exists if(self.operating_system == 'Windows'): return False return os.path.exists(flagpath) elif(state == 1): # create if(self.operating_system == 'Windows'): return open(flagpath, 'w').close() self.ser_port_lock_path = flagpath elif(state == 2): # erase if(self.operating_system == 'Windows'): return if(os.path.exists(self.ser_port_lock_path)): os.remove(self.ser_port_lock_path) self.ser_port_lock_path = "" def close_serial(self): if(self.ser_handle): self.ser_handle.close() self.ser_handle = False self.serial_flag_control(2) # delete old flag def init_serial(self,*args): if(self.k_port_combobox.read() != self.old_port \ or self.k_baud_combobox.read() != self.old_baud): self.set_term_color(True) confs = "port %s, rate %s" % (self.k_port_combobox.read(),self.k_baud_combobox.read()) try: self.close_serial() # test if flag already esists if(self.serial_flag_control(0,self.k_port_combobox.read())): raise Exception("Port %s in use" % self.k_port_combobox.read()) port = self.k_port_combobox.read() if(self.operating_system == 'Linux'): port = "/dev/" + port self.ser_handle = serial.Serial(port, \ self.k_baud_combobox.read(),parity = serial.PARITY_NONE,timeout=0,rtscts=0) self.show_status("Sucessful serial port intialization: %s" % confs) self.serial_flag_control(1,self.k_port_combobox.read()) # create new flag except Exception as e: self.close_serial() self.show_status("Serial initialization failed: %s. Reason: %s" % (confs,e)) self.set_term_color(False) self.old_port = self.k_port_combobox.read() self.old_baud = self.k_baud_combobox.read() def timed_com(self,a): self.keybuf += a.pop(0) + "\n" return(len(a) > 0) def com_string(self,s): array = s.split("|") if(len(array) > 1): self.timed_com(array) gobject.timeout_add(500,lambda: self.timed_com(array)) else: self.keybuf += s + "\n" def push_comline(self,s): self.comline_history.append(s) self.comline_index = len(self.comline_history) def com_keyboard_read(self,w,evt): returnval = False com_entry = self.k_com_entry.inst top = len(self.comline_history) cn = gtk.gdk.keyval_name(evt.keyval) if(cn == 'Return'): s = com_entry.get_text() if(len(s) > 0): self.keybuf += s + "\n" if(self.comline_index == 0 or s != self.comline_history[self.comline_index-1]): self.push_comline(s) self.comline_current = "" com_entry.set_text("") returnval = True elif(cn == 'Up'): if(self.comline_index > 0): if(self.comline_index == top): self.comline_current = com_entry.get_text() self.comline_index -= 1 com_entry.set_text(self.comline_history[self.comline_index]) returnval = True elif(cn == 'Down'): if(self.comline_index < top): self.comline_index += 1 if(self.comline_index < top): com_entry.set_text(self.comline_history[self.comline_index]) else: com_entry.set_text(self.comline_current) returnval = True if(returnval == True): com_entry.set_position(-1) else: self.comline_current = com_entry.get_text() return returnval def main_keyboard_read(self,w,evt): c = evt.keyval cn = gtk.gdk.keyval_name(c) if (evt.state & gtk.gdk.CONTROL_MASK): c &= 31 if (cn in self.keymap): self.keybuf += self.keymap[cn] elif(c < 256): self.keybuf += chr(c) return True def process_keys(self): if(len(self.keybuf) > 0): s = self.keybuf self.keybuf = "" if(self.k_echo_checkbutton.read()): # don't echo terminal control chars fs = re.sub("\033\[\w","",s) self.insert_text(fs) if(self.k_outcr_checkbutton.read()): s = re.sub("\n","\r\n",s) if(self.ser_handle): self.ser_handle.write(s) def process_io(self): self.process_keys() if(self.ser_handle): s = self.ser_handle.read(4096) if(len(s) > 0): if not (self.k_incr_checkbutton.read()): s = re.sub("\r","",s) self.insert_text(s) if(self.loghandle): self.loghandle.flush() return(True) # just once def init_textbuffer(self): self.term_window.set_cursor_visible(False) self.tbuff.set_text(' ') # the cursor char self.cursor_tag = self.tbuff.create_tag() self.cursor_tag.set_property('background','black') eb = self.tbuff.get_end_iter() ea = eb.copy() ea.backward_char() self.tbuff.apply_tag(self.cursor_tag,ea,eb) def insert_text(self,s): if(len(s) > 0): try: ei = self.tbuff.get_end_iter() ei.backward_char() # behind the cursor character for c in s: oc = ord(c) if(oc == self.CHAR_BS): self.tbuff.backspace(ei,False,True) else: if (oc >= 32 or oc == self.CHAR_LF or oc == self.CHAR_CR): # python 3: u = str(c,'utf-8') u = unicode(c,'utf-8') self.tbuff.insert(ei,u) self.write_log(u) self.scroll_to_bottom() except Exception as e: # pass print('Error %s' % e) def scroll_to_bottom(self): if(self.k_scroll_checkbutton.read()): im = self.tbuff.get_insert() isb = self.tbuff.get_selection_bound() if(im != None and isb != None): self.tbuff.move_mark(im,self.tbuff.get_end_iter()) self.tbuff.move_mark(isb,self.tbuff.get_end_iter()) self.term_window.scroll_mark_onscreen(im) # end of SerialTerminal class app=SerialTerminal() gtk.main()