| Home | | Python | | ![]() | ![]() | Share This Page |
A Python Serial interface terminal
— P. Lutus — Message Page —
Copyright © 2010, P. Lutus
Discussion | Licensing, Source | Revision History(double-click any word to see its definition)
Most people who use serial interfaces on small computers must rely on USB-to-serial adaptors, since the old-style serial connectors have vanished and few find a use for serial interfaces any more — they are slow, relatively unsophisticated, and a bit tricky to set up.
But there are a few niche applications — anything involving GPS will likely need a serial communications channel at some point. And most real terminals (not virtual ones) communicate with a serial interface.
I have always needed a serial terminal for one reason or another, some having to do with owning a boat, or being a radio amateur, or wanting to communicate with a shipboard satellite dish. Years ago I simply kept a serial terminal around — glass screen, heavy, you know. But some years ago I decided to virtualize this, so any handy computer could stand in for a serial terminal.
Since then I have written any number of serial terminal programs, most not very good. This Python program is by far the best — it has more features and is more robust in the face of unexpected circumstances.
Like some other recent programs, this one uses the GTK toolkit and I designed its GUI with Glade. This program has some features that are likely only to be of interest to me — for example, a subpanel of satellite dish commands for the dish on my boat. Users are obviously free to redefine or remove this menu, which is not displayed by default.
This terminal program offers the usual kinds of serial setup options like baud rate and port, and it is very fast — I have successfully transferred text files at the highest available speed (230,400 baud) with no data loss. But because of the possible applications for a terminal program, it also offers the slowest imaginable baud rate (50 baud). At 50 baud you can almost tell what's being sent by listening to the line noise.
Becasue this is a terminal program, the keyboard's output is captured and dedicated to the I/O stream, and is therefore unavailable for typical program uses. In practical terms, this means you can't use Ctrl+C to copy text from the display. To copy text, either use the context menu "Copy" function, or use the program's logging feature.
Here is one setup I used to test the program — it uses one computer as a serial server and another as a client, running this program. Here is the script for the server side:
#!/bin/sh port="ttyUSB0" [ -n "$1" ] && port=$1 trap "{ /sbin/fuser -k /dev/$port; exit 0; }" EXIT baud=230400 term=raw while true do echo -n "`date`: Hosting $baud baud $term serial process on $port ... " /sbin/agetty -hL $baud $port $term /sbin/fuser -k /dev/$port echo -e "\n`date`: Closed serial session." doneWith the above script running on one machine and a null-modem cable between two USB-to-serial converters, I was able to figure out what my program's weaknesses were and correct them. In particular I wanted to see if I could transfer text data without errors at 230,400 baud — this repeated test helped the design.
I don't have any shortage of computers, but it occurs to me that one could run tests with just one machine and two serial interfaces, by connecting the above script on one interface and this terminal program on the other, and a null-modem serial cable conecting two USB adaptors.
Program Notes
Threads
First and most important, a program like this really should be run on multiple threads. I wrote a serial terminal program before this one in Ruby and it used threads. It seem Ruby's thread handling is superior to Python's. The problem was that Ruby doesn't have a serial library, so I was reduced to calling platform-specific utilities to set things up, and there were times when the serial setup would fail mysteriously.
Much of my time designing this program was spent trying to get Python threads to work. Eventually I realized the problem was insoluble and is caused by Python's crappy thread handling, so I implemented a solution using a timer instead. In line 304 of the listing below I set up a recurring timer with an interval of 100 milliseconds, which calls a data processing function that reads the keyboard and serial port. This seems to work fine, even at the highest data rates. And, unlike the threaded version, there are no more core dumps every thirty seconds.
GUI interfacing
Originally I had a typical small-program plan in which I would read the values from the GTK controls in the GUI interface and copy the values to local variables for use within the terminal program. But as the program grew, this scheme became somewhat unwieldy, so I chose a more convenient scheme in which the class instance has an overriden __getitem__() function that allows the instance to be its own list of widgets, as shown in this program.
But eventually I designed a more powerful and systematic approach in which some subsidiary classes manage the GTK widgets and their user-selected values, and create references to the widgets in the parent program. This means all the GTK widgets of interest appear as class variables in the main class, and they can be read and written in a convenient way.
In the present embodiment, the ConfigManager class (lines 147-187) creates a list of GTK widgets that I think will be useful as local variables (I signal this by giving them a prefix of "k_" in the Glade interface designer), reads a configuration file to restore the values from the prior session, and creates a reference to each widget in the parent class.
The references are instances of a class named ControlInterface (lines 90-145), which manages reading and writing values to the GTK widgets in a convenient way.
The advantage to this setup is that I can add or remove widgets as the design progresses, and the infrastructure automatically handles the details including writing and reading a configuration file so the user's choices are preserved between runs. It's very simple — if I think a widget's value or state is needed by my program, I give it a name starting with "k_" and a reference automatically appears in my main class with the same name. And if I change the value in the main program, the ControlInterface class updates the control itself.
When I exit the program, the ConfigManager class saves a configuration file containing the states and values for all the selected widgets. When I run the program again, all the prior values are restored. This is especially important in a serial terminal program, where so many settings are required to get the communication channel just right.
Satellite mode
A subpanel, hidden by default, lets me configure my boat's satellite dish. It also switches entry modes — in the default mode, one enters text into the main display, like a shell session. But the satellite controller is constantly listing information about its state, so it's difficult to type into this scrolling display. This is why the satellite mode has a separate field for entering commands. The one-line entry field has a history feaure — just press the up-arrow key to recover past entries.
Locking problem
At the time of writing I have just installed Fedora 14, which has changed the setup for preventing multiple accesses to serial ports. The old arrangement for a given port was:
- Check to see if there is an empty file named "/var/lock/LCK..(port name)", for example for /dev/ttyUSB0 it would be "/var/lock/LCK..ttyUSB0". The presence of this file meant the serial port was busy and inaccessibe.
- If there was no such file, one would create the file, open the port, then remove the file once the port was no longer neded.
This scheme worked because the /var/lock directory belonged to the lock group, and one only needed to be a lock group member to write to it. For some reason, the Fedora developers decided this was way too easy, and they have changed the permissions so the /var/lock directory is owned by root and only user writable, so ordinary users can no longer write to it. The apparent long-term goal is to move lock files to /var/lock/lockdev, but I haven't done this for the simple reason that I have a number of serial interface programs that contend for this resource, not all under my control, and they all expect the flags to be located in /var/lock.
So I offer this temporary fix, which keeps things running until the developers think of a more coordinated plan. As root:
# chgrp lock /dev/lock # chmod g+w /dev/lockAlso, users of serial terminals and related programs need to be members of the lock group. As root:
# usermod -a -G lock (user-name)
Licensing, Source
SerialTerminal is released under the GNU General Public License.
Here is the plain-text source file without line numbers.
Here is the Glade GTKBuilder XML configuration file (required).
Revision History
- Version 1.5 10/03/2011. Changed the configuration load/save routine to include window size.
- Version 1.4 01/15/2011. Corrected some small bugs.
- Version 1.3 12/15/2010. Made some improvements, added a program icon in XPM format within the program listing.
- Version 1.2 12/09/2010. Changed from libglade to GTKBuilder library, made a number of improvements.
- Version 1.1 12/08/2010. Changed how the program manages satellite mode.
- Version 1.0 12/07/2010. Initial Public Release.
Program Listing
1: #!/usr/bin/env python
2: # -*- coding: utf-8 -*-
3:
4: # ***************************************************************************
5: # * Copyright (C) 2011, Paul Lutus *
6: # * *
7: # * This program is free software; you can redistribute it and/or modify *
8: # * it under the terms of the GNU General Public License as published by *
9: # * the Free Software Foundation; either version 2 of the License, or *
10: # * (at your option) any later version. *
11: # * *
12: # * This program is distributed in the hope that it will be useful, *
13: # * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14: # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15: # * GNU General Public License for more details. *
16: # * *
17: # * You should have received a copy of the GNU General Public License *
18: # * along with this program; if not, write to the *
19: # * Free Software Foundation, Inc., *
20: # * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
21: # ***************************************************************************
22:
23: # version date 10-03-2011
24:
25: VERSION = '1.5'
26:
27: # no serial support in python 3!
28:
29: import os, re, sys, signal
30: import pango
31: import serial
32: import gtk
33: import gobject
34: import platform
35: import webbrowser
36:
37: class Icon:
38: icon = [
39: "32 32 17 1",
40: " c None",
41: ". c #000100",
42: "+ c #1A1B19",
43: "@ c #252827",
44: "# c #424443",
45: "$ c #4F504E",
46: "% c #425F7F",
47: "& c #4B6F97",
48: "* c #6D6E6C",
49: "= c #5B7FA7",
50: "- c #818382",
51: "; c #919391",
52: "> c #7B96B7",
53: ", c #AEB0AD",
54: "' c #CBCDCB",
55: ") c #E2E4E1",
56: "! c #EFF1EE",
57: " ",
58: " ",
59: " ........................... ",
60: " .!!!!!!!!!!!!!!!!!))))))'',. ",
61: " .!)!!!!!!!!!!!))))))))'''';. ",
62: " .!)*....................*'-. ",
63: " .!!.&&&&&&&&&&&&&&&&&&&%@'-. ",
64: " .!).&>>>>>>>>>>>>>>>>>>=+'-. ",
65: " .!).&>>>>>>>>>>>>>>>>>>=+'-. ",
66: " .!).&>>>>>>>>>>>>>>>====+,-. ",
67: " .!).&>>>>>>>>>>=========+,-. ",
68: " .!).&>>>&&&&&&&&&%%%&&&=+,-. ",
69: " .)).&>>&''''''''''''%&&=+,-. ",
70: " .)'.&>>&''-))))))))'%&&=+,-. ",
71: " .)'.&&&%''-))))))))'%&&=+,-. ",
72: " .)'.&&&%'''''''''',.%&&>+,-.. ",
73: " .)'.&&&&%%%%%%%%%%.'.%&>+,*.,.",
74: " .''.&&&&&&&&&&&&&&&.!.&>+;.'. ",
75: " .''.&==========&.==.!.&>+*.. ",
76: " .''.&==========.'..!).%>..$. ",
77: " .',.=>>>>>>>>>>>.'))),.#@$-. ",
78: " .',$.................'$.#;-. ",
79: " .',,,,,,,,,,,,,,,,,,,.@#.;-. ",
80: " .,,,,,,,,,,,,,,,;;...#.'*.*. ",
81: " .,;;;;;;;;;;;;;;;.-'>.;.'-.. ",
82: " ...$$$$$$$$$$$$$.>'>>@##.!*. ",
83: " +.*----;;;;;;-.;'>>%.***.'*. ",
84: " ..............'>>%.......'$ ",
85: " .>=%. .. ",
86: " ... ",
87: " ",
88: " "
89: ]
90:
91:
92: # a convenience class for
93: # communicating with gtk controls
94:
95: class ControlInterface:
96:
97: # pretend enumeration
98: IS_BOOL,IS_COMBO,IS_STRING,IS_WINDOW,IS_UNKNOWN = list(range(5))
99:
100: def __init__(self,obj,name):
101: self.inst = obj
102: self.name = name
103: self.combo_array = None
104: typ = type(obj)
105: if(typ == gtk.RadioButton or typ == gtk.CheckButton):
106: self.cat = ControlInterface.IS_BOOL
107: elif(typ == gtk.ComboBox):
108: # if no cell renderer
109: if(len(obj.get_cells()) == 0):
110: # Create a text cell renderer
111: cell = gtk.CellRendererText ()
112: obj.pack_start(cell)
113: obj.add_attribute (cell, "text", 0)
114: self.cat = ControlInterface.IS_COMBO
115: elif(typ == gtk.Entry):
116: self.cat = ControlInterface.IS_STRING
117: elif(typ == gtk.Window):
118: self.cat = ControlInterface.IS_WINDOW
119: else:
120: self.cat = ControlInterface.IS_UNKNOWN
121:
122: def read(self):
123: if(self.cat == ControlInterface.IS_BOOL):
124: return self.inst.get_active()
125: elif(self.cat == ControlInterface.IS_COMBO):
126: return self.inst.get_active_text()
127: elif(self.cat == ControlInterface.IS_STRING):
128: return self.inst.get_text()
129: elif(self.cat == ControlInterface.IS_WINDOW):
130: return self.inst.get_size()
131: else:
132: return None
133:
134: def write(self,v):
135: if(self.cat == ControlInterface.IS_BOOL):
136: self.inst.set_active(str(v) == 'True')
137: elif(self.cat == ControlInterface.IS_COMBO):
138: if(v in self.combo_array):
139: index = self.combo_array.index(v)
140: self.inst.set_active(index)
141: else:
142: self.inst.set_active(0)
143: elif(self.cat == ControlInterface.IS_STRING):
144: self.inst.set_text(v)
145: elif(self.cat == ControlInterface.IS_WINDOW):
146: x,y = re.findall('\d+',v)
147: self.inst.resize(int(x),int(y))
148: else:
149: raise Exception("Trying to write data to unknown control type ", self.inst)
150:
151: def load_combo_list(self,ca):
152: assert(self.cat == ControlInterface.IS_COMBO)
153: # keep this list for later access
154: self.combo_array = ca
155: self.inst.get_model().clear()
156: for s in ca:
157: self.inst.append_text(s.strip())
158:
159: class ConfigManager:
160:
161: def __init__(self,inst):
162: self.parent = inst
163: # config file located at (user home dir)/.classname
164: self.configpath = os.path.expanduser("~/." + self.parent.__class__.__name__)
165: self.read_widgets()
166:
167: # do this only once
168: def read_widgets(self):
169: # an unbelievable hack made necessary by
170: # someone unwilling to fix a year-old bug
171: with open(self.parent.xmlname) as f:
172: data = f.read()
173: array = re.findall(r'(?s) id="(.*?)"',data)
174: for name in array:
175: # only interested in names starting with 'k_'
176: if re.search(r'^k_',name):
177: obj = self.parent.builder.get_object(name)
178: ci = ControlInterface(obj,name)
179: # create parent reference with same name
180: setattr(self.parent,name,ci)
181:
182: def read_config(self):
183: if(os.path.exists(self.configpath)):
184: with open(self.configpath) as f:
185: for line in f.readlines():
186: name,value = re.search(r'^\s*(.*?)\s*=\s*(.*?)\s*$',line).groups()
187: ci = getattr(self.parent,name,None)
188: if(ci != None):
189: ci.write(value)
190: else:
191: print("no object named %s" % name)
192:
193: def write_config(self):
194: with open(self.configpath,'w') as f:
195: for name in dir(self.parent):
196: if re.search(r'^k_',name):
197: ci = getattr(self.parent,name,None)
198: if(ci != None and ci.read() != None):
199: f.write("%s = %s\n" % (ci.name,ci.read()))
200:
201: class SerialTerminal:
202:
203: def __init__(self):
204: self.builder = gtk.Builder()
205: self.xmlname = "serialterminal_gui31.glade"
206: self.builder.add_from_file(self.xmlname)
207: self.operating_system = platform.system()
208: self.white = gtk.gdk.color_parse("white")
209: self.gray = gtk.gdk.color_parse("#e0e0e0")
210: self.black = gtk.gdk.color_parse("black")
211: self.CHAR_BELL = 7
212: self.CHAR_BS = 8
213: self.CHAR_LF = 10
214: self.CHAR_CR = 13
215: self.CHAR_TAB = 9
216: self.program_name = self.__class__.__name__
217: self.title = self.program_name + ' ' + VERSION
218: self.logfn = self.program_name + ".log"
219: self.loghandle = False
220: self.running = False
221: self.ser_handle = False
222: self.old_satmode = False
223: self.old_port = ""
224: self.old_baud = ""
225: self.ser_port_lock_path = ""
226: self.keybuf = ""
227: self.comline_history = []
228: self.comline_index = 0
229: self.comline_current = ""
230: self.com_key_event = False
231: self.main_key_event = False
232:
233: sat_data = (
234: "HALT",
235: "@DEBUGON",
236: "@SATCONFIG,C,54,12516,20000,99,0X1008,R,U,3",
237: "@SATCONFIG,C,54,12516,20000,99,0X1008,R,U,3",
238: "@SATCONFIG,C,54,12676,20000,99,0X1008,L,U,3",
239: "@SATCONFIG,C,54,12676,20000,99,0X1008,L,U,3",
240: "@SAVE,C",
241: "@DEBUGOFF",
242: "@SATCONFIG",
243: )
244: self.sat_program = '|'.join(sat_data)
245:
246: baud_rates = (
247: "50",
248: "75",
249: "110",
250: "134",
251: "150",
252: "200",
253: "300",
254: "600",
255: "1200",
256: "1800",
257: "2400",
258: "4800",
259: "9600",
260: "19200",
261: "38400",
262: "57600",
263: "115200",
264: "230400",
265: )
266: self.keymap = {
267: 'Return' : chr(self.CHAR_LF),
268: 'BackSpace' : chr(self.CHAR_BS),
269: 'Tab' : chr(self.CHAR_TAB),
270: 'Up' : '\033[A',
271: 'KP_Up' : '\033[A',
272: 'Down' : '\033[B',
273: 'KP_Down' : '\033[B',
274: 'Right' : '\033[C',
275: 'KP_Right' : '\033[C',
276: 'Left' : '\033[D',
277: 'KP_Left' : '\033[D',
278: 'Home' : '\033[H',
279: 'KP_Home' : '\033[H',
280: 'End' : '\033[4~',
281: 'KP_End' : '\033[4~',
282: }
283:
284: self.config = ConfigManager(self)
285: # now we have local names for gtk objects of interest
286: self.k_size_combobox.load_combo_list([str(x) for x in range(4,65,2)])
287: self.k_baud_combobox.load_combo_list(baud_rates)
288: if(self.operating_system == 'Windows'):
289: devs = ['COM%d' % dev for dev in range(1,9)]
290: else:
291: devs = [dev for dev in sorted(os.listdir("/dev")) if (re.search('tty(U|S)',dev))]
292: self.k_port_combobox.load_combo_list(devs)
293: # initial must-have values
294: self.k_baud_combobox.write('4800')
295: self.k_port_combobox.write('ttyUSB0')
296: self.k_size_combobox.write('10')
297: # now read configuration file if exists
298: self.config.read_config()
299: self.mainwindow = self.k_ser_term.inst
300: self.mainwindow.set_title(self.title)
301: self.mainwindow.set_icon(gtk.gdk.pixbuf_new_from_xpm_data(Icon.icon))
302: self.term_window = self.k_term_window.inst
303: self.tbuff = self.term_window.get_buffer()
304: self.term_window.modify_bg(gtk.STATE_NORMAL,self.white)
305:
306: # gracefully exit on keyboard and other signals
307:
308: signal.signal(signal.SIGTERM, self.close)
309: signal.signal(signal.SIGINT, self.close)
310:
311: # connect controls to actions
312: self.connect_task()
313: self.init_textbuffer()
314: self.toggle_control() # set defaults
315:
316: self.init_serial()
317: self.running = True
318: # periodically read/write
319: gobject.timeout_add(100,self.process_io)
320:
321: def connect_task(self):
322: connect_list = (
323: (self.k_quit_button.inst,'clicked',self.close),
324: (self.k_help_button.inst,'clicked',self.launch_help),
325: (self.k_ser_term.inst,'unrealize',self.close),
326: (self.k_scroll_checkbutton.inst,'toggled',self.toggle_control),
327: (self.k_echo_checkbutton.inst,'toggled',self.toggle_control),
328: (self.k_wrap_checkbutton.inst,'toggled',self.toggle_control),
329: (self.k_log_checkbutton.inst,'toggled',self.toggle_control),
330: (self.k_satmode_checkbutton.inst,'toggled',self.toggle_control),
331: (self.k_latnbutton.inst,'toggled',self.toggle_control),
332: (self.k_lngwbutton.inst,'toggled',self.toggle_control),
333: (self.k_lat_entry.inst,'changed',self.toggle_control),
334: (self.k_lng_entry.inst,'changed',self.toggle_control),
335: (self.k_size_combobox.inst,'changed',self.toggle_control),
336: (self.k_baud_combobox.inst,'changed',self.init_serial),
337: (self.k_port_combobox.inst,'changed',self.init_serial),
338: (self.k_clear_log_button.inst,'clicked',self.clear_log),
339: (self.k_ser_term.inst,'focus-in-event', \
340: lambda w,e: self.focus_control(True)),
341: (self.k_ser_term.inst,'focus-out-event', \
342: lambda w,e: self.focus_control(False)),
343: # trap chars headed for text display
344: (self.k_term_window.inst,'key-press-event', lambda w,e: True),
345: (self.k_e119button.inst,'clicked',lambda w: self.com_string("@L,A")),
346: (self.k_e110button.inst,'clicked',lambda w: self.com_string("@L,B")),
347: (self.k_e129button.inst,'clicked',lambda w: self.com_string("@L,C")),
348: (self.k_sleeponbutton.inst,'clicked', \
349: lambda w: self.com_string("DEBUGON|SLEEPON|DEBUGOFF")),
350: (self.k_sleepoffbutton.inst,'clicked', \
351: lambda w: self.com_string("DEBUGON|SLEEPOFF|DEBUGOFF")),
352: (self.k_program_button.inst,'clicked', \
353: lambda w: self.com_string(self.sat_program)),
354: (self.k_list_button.inst,'clicked', \
355: lambda w: self.com_string("@SATCONFIG")),
356: (self.k_setposbutton.inst,'clicked',self.send_pos_string),
357: )
358: for tup in connect_list:
359: tup[0].connect(tup[1],tup[2])
360:
361: def focus_control(self,focus):
362: col = ('gray','black')[focus]
363: self.cursor_tag.set_property('background',col)
364:
365: def show_status(self,s):
366: self.k_status_label.inst.set_text(s)
367:
368: def set_term_color(self,normal):
369: col = (self.gray,self.white)[normal]
370: self.term_window.modify_base(gtk.STATE_NORMAL,col)
371:
372: def launch_help(self,*args):
373: webbrowser.open("http://arachnoid.com/python/serialterminal_program.html")
374:
375: def close(self,*args):
376: self.running = False
377: self.close_serial()
378: self.config.write_config()
379: self.control_log(False)
380: gtk.main_quit()
381:
382: def toggle_control(self,*args):
383: wm = gtk.WRAP_NONE
384: if(self.k_wrap_checkbutton.read()): wm = gtk.WRAP_WORD_CHAR
385: self.term_window.set_wrap_mode(wm)
386: self.scroll_to_bottom()
387: self.control_log(self.k_log_checkbutton.read())
388: fontsz = self.k_size_combobox.read()
389: font = pango.FontDescription("Monospace,%s" % fontsz)
390: self.term_window.modify_font(font)
391: self.k_extension_vbox.inst.set_visible(self.k_satmode_checkbutton.read())
392: if(not self.running or self.old_satmode != self.k_satmode_checkbutton.read()):
393: if(self.k_satmode_checkbutton.read()): # changing to sat mode
394: if(self.main_key_event):
395: self.k_ser_term.inst.disconnect(self.main_key_event)
396: self.com_key_event = self.k_com_entry.inst.connect('key-press-event', \
397: self.com_keyboard_read)
398: self.k_com_entry.inst.connect('key-release-event', lambda w,e: True)
399: self.k_com_entry.inst.grab_focus()
400: if(self.running):
401: self.k_outcr_checkbutton.write(True)
402: self.k_incr_checkbutton.write(False)
403: self.k_baud_combobox.write('9600')
404: self.init_serial()
405: else: # changing to terminal mode
406: if(self.com_key_event):
407: self.k_com_entry.inst.disconnect(self.com_key_event)
408: self.main_key_event = self.k_ser_term.inst.connect('key-press-event', \
409: self.main_keyboard_read)
410: self.old_satmode = self.k_satmode_checkbutton.read()
411:
412: def send_pos_string(self,*args):
413: pos = "GPS,%s,%s,%s,%s" % (
414: self.k_lat_entry.read(),
415: ('S','N')[self.k_latnbutton.read()],
416: self.k_lng_entry.read(),
417: ('E','W')[self.k_lngwbutton.read()],
418: )
419: s = "DEBUGON|%s|DEBUGOFF" % pos
420: self.com_string(s)
421:
422: def clear_log(self,*args):
423: oldstate = self.k_log_checkbutton.read()
424: self.control_log(False)
425: f = open(self.logfn,'w')
426: f.close()
427: self.control_log(oldstate)
428:
429: def control_log(self,state):
430: self.k_log_checkbutton.write(state)
431: if(self.k_log_checkbutton.read()):
432: if not (self.loghandle):
433: self.loghandle = open(self.logfn,'a')
434: else:
435: if(self.loghandle):
436: self.loghandle.close()
437: self.loghandle = False
438:
439: def write_log(self,s):
440: if(self.loghandle):
441: self.loghandle.write(s)
442:
443: def create_lock_path(self,port):
444: return "/var/lock/lockdev/LCK..%s" % self.k_port_combobox.read()
445:
446: def serial_flag_control(self,state,port=""):
447: flagpath = self.create_lock_path(port)
448: if(state == 0): # check if exists
449: if(self.operating_system == 'Windows'): return False
450: return os.path.exists(flagpath)
451: elif(state == 1): # create
452: if(self.operating_system == 'Windows'): return
453: open(flagpath, 'w').close()
454: self.ser_port_lock_path = flagpath
455: elif(state == 2): # erase
456: if(self.operating_system == 'Windows'): return
457: if(os.path.exists(self.ser_port_lock_path)): os.remove(self.ser_port_lock_path)
458: self.ser_port_lock_path = ""
459:
460: def close_serial(self):
461: if(self.ser_handle):
462: self.ser_handle.close()
463: self.ser_handle = False
464: self.serial_flag_control(2) # delete old flag
465:
466: def init_serial(self,*args):
467: if(self.k_port_combobox.read() != self.old_port \
468: or self.k_baud_combobox.read() != self.old_baud):
469: self.set_term_color(True)
470: confs = "port %s, rate %s" % (self.k_port_combobox.read(),self.k_baud_combobox.read())
471: try:
472: self.close_serial()
473: # test if flag already esists
474: if(self.serial_flag_control(0,self.k_port_combobox.read())):
475: raise Exception("Port %s in use" % self.k_port_combobox.read())
476: port = self.k_port_combobox.read()
477: if(self.operating_system == 'Linux'):
478: port = "/dev/" + port
479: self.ser_handle = serial.Serial(port, \
480: self.k_baud_combobox.read(),parity = serial.PARITY_NONE,timeout=0,rtscts=0)
481: self.show_status("Sucessful serial port intialization: %s" % confs)
482: self.serial_flag_control(1,self.k_port_combobox.read()) # create new flag
483: except Exception as e:
484: self.close_serial()
485: self.show_status("Serial initialization failed: %s. Reason: %s" % (confs,e))
486: self.set_term_color(False)
487: self.old_port = self.k_port_combobox.read()
488: self.old_baud = self.k_baud_combobox.read()
489:
490: def timed_com(self,a):
491: self.keybuf += a.pop(0) + "\n"
492: return(len(a) > 0)
493:
494: def com_string(self,s):
495: array = s.split("|")
496: if(len(array) > 1):
497: self.timed_com(array)
498: gobject.timeout_add(500,lambda: self.timed_com(array))
499: else:
500: self.keybuf += s + "\n"
501:
502: def push_comline(self,s):
503: self.comline_history.append(s)
504: self.comline_index = len(self.comline_history)
505:
506: def com_keyboard_read(self,w,evt):
507: returnval = False
508: com_entry = self.k_com_entry.inst
509: top = len(self.comline_history)
510: cn = gtk.gdk.keyval_name(evt.keyval)
511: if(cn == 'Return'):
512: s = com_entry.get_text()
513: if(len(s) > 0):
514: self.keybuf += s + "\n"
515: if(self.comline_index == 0 or s != self.comline_history[self.comline_index-1]):
516: self.push_comline(s)
517: self.comline_current = ""
518: com_entry.set_text("")
519: returnval = True
520: elif(cn == 'Up'):
521: if(self.comline_index > 0):
522: if(self.comline_index == top):
523: self.comline_current = com_entry.get_text()
524: self.comline_index -= 1
525: com_entry.set_text(self.comline_history[self.comline_index])
526: returnval = True
527: elif(cn == 'Down'):
528: if(self.comline_index < top):
529: self.comline_index += 1
530: if(self.comline_index < top):
531: com_entry.set_text(self.comline_history[self.comline_index])
532: else:
533: com_entry.set_text(self.comline_current)
534: returnval = True
535: if(returnval == True):
536: com_entry.set_position(-1)
537: else:
538: self.comline_current = com_entry.get_text()
539: return returnval
540:
541: def main_keyboard_read(self,w,evt):
542: c = evt.keyval
543: cn = gtk.gdk.keyval_name(c)
544: if (evt.state & gtk.gdk.CONTROL_MASK):
545: c &= 31
546: if (cn in self.keymap):
547: self.keybuf += self.keymap[cn]
548: elif(c < 256):
549: self.keybuf += chr(c)
550: return True
551:
552: def process_keys(self):
553: if(len(self.keybuf) > 0):
554: s = self.keybuf
555: self.keybuf = ""
556: if(self.k_echo_checkbutton.read()):
557: # don't echo terminal control chars
558: fs = re.sub("\033\[\w","",s)
559: self.insert_text(fs)
560: if(self.k_outcr_checkbutton.read()):
561: s = re.sub("\n","\r\n",s)
562: if(self.ser_handle):
563: self.ser_handle.write(s)
564:
565: def process_io(self):
566: self.process_keys()
567: if(self.ser_handle):
568: s = self.ser_handle.read(4096)
569: if(len(s) > 0):
570: if not (self.k_incr_checkbutton.read()):
571: s = re.sub("\r","",s)
572: self.insert_text(s)
573: if(self.loghandle):
574: self.loghandle.flush()
575: return(True)
576:
577: # just once
578: def init_textbuffer(self):
579: self.term_window.set_cursor_visible(False)
580: self.tbuff.set_text(' ') # the cursor char
581: self.cursor_tag = self.tbuff.create_tag()
582: self.cursor_tag.set_property('background','black')
583: eb = self.tbuff.get_end_iter()
584: ea = eb.copy()
585: ea.backward_char()
586: self.tbuff.apply_tag(self.cursor_tag,ea,eb)
587:
588: def insert_text(self,s):
589: if(len(s) > 0):
590: try:
591: ei = self.tbuff.get_end_iter()
592: ei.backward_char() # behind the cursor character
593: for c in s:
594: oc = ord(c)
595: if(oc == self.CHAR_BS):
596: self.tbuff.backspace(ei,False,True)
597: else:
598: if (oc >= 32 or oc == self.CHAR_LF or oc == self.CHAR_CR):
599: # python 3: u = str(c,'utf-8')
600: u = unicode(c,'utf-8')
601: self.tbuff.insert(ei,u)
602: self.write_log(u)
603: self.scroll_to_bottom()
604: except Exception as e:
605: # pass
606: print('Error %s' % e)
607:
608: def scroll_to_bottom(self):
609: if(self.k_scroll_checkbutton.read()):
610: im = self.tbuff.get_insert()
611: isb = self.tbuff.get_selection_bound()
612: if(im != None and isb != None):
613: self.tbuff.move_mark(im,self.tbuff.get_end_iter())
614: self.tbuff.move_mark(isb,self.tbuff.get_end_iter())
615: self.term_window.scroll_mark_onscreen(im)
616:
617: # end of SerialTerminal class
618:
619: app=SerialTerminal()
620: gtk.main()
621:
| Home | | Python | | ![]() | ![]() | Share This Page |