blob: 47ee698859b13601b81da11b8cadd90cbaca2a27 [file] [log] [blame]
cliechti80a0ed12003-10-03 23:53:42 +00001#!/usr/bin/env python
Chris Liechtifbdd8a02015-08-09 02:37:45 +02002#
3# A simple terminal application with wxPython.
4#
5# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
6#
7# SPDX-License-Identifier: BSD-3-Clause
cliechti80a0ed12003-10-03 23:53:42 +00008
cliechtid5d51982008-04-10 23:48:55 +00009#from wxPython.wx import *
10import wx
cliechti80a0ed12003-10-03 23:53:42 +000011import wxSerialConfigDialog
12import serial
13import threading
14
cliechti40d71f62004-07-09 22:14:17 +000015#----------------------------------------------------------------------
16# Create an own event type, so that GUI updates can be delegated
17# this is required as on some platforms only the main thread can
18# access the GUI without crashing. wxMutexGuiEnter/wxMutexGuiLeave
19# could be used too, but an event is more elegant.
20
cliechtid5d51982008-04-10 23:48:55 +000021SERIALRX = wx.NewEventType()
cliechtibc318d42004-11-13 03:13:12 +000022# bind to serial data receive events
cliechtid5d51982008-04-10 23:48:55 +000023EVT_SERIALRX = wx.PyEventBinder(SERIALRX, 0)
cliechti40d71f62004-07-09 22:14:17 +000024
cliechtid5d51982008-04-10 23:48:55 +000025class SerialRxEvent(wx.PyCommandEvent):
cliechti40d71f62004-07-09 22:14:17 +000026 eventType = SERIALRX
27 def __init__(self, windowID, data):
cliechtid5d51982008-04-10 23:48:55 +000028 wx.PyCommandEvent.__init__(self, self.eventType, windowID)
cliechti40d71f62004-07-09 22:14:17 +000029 self.data = data
30
31 def Clone(self):
32 self.__class__(self.GetId(), self.data)
33
34#----------------------------------------------------------------------
35
cliechtid5d51982008-04-10 23:48:55 +000036ID_CLEAR = wx.NewId()
37ID_SAVEAS = wx.NewId()
38ID_SETTINGS = wx.NewId()
39ID_TERM = wx.NewId()
40ID_EXIT = wx.NewId()
cliechti80a0ed12003-10-03 23:53:42 +000041
42NEWLINE_CR = 0
43NEWLINE_LF = 1
44NEWLINE_CRLF = 2
45
46class TerminalSetup:
cliechti0eb86712003-10-04 00:49:04 +000047 """Placeholder for various terminal settings. Used to pass the
48 options to the TerminalSettingsDialog."""
cliechti80a0ed12003-10-03 23:53:42 +000049 def __init__(self):
50 self.echo = False
51 self.unprintable = False
52 self.newline = NEWLINE_CRLF
53
cliechtid5d51982008-04-10 23:48:55 +000054class TerminalSettingsDialog(wx.Dialog):
cliechti0eb86712003-10-04 00:49:04 +000055 """Simple dialog with common terminal settings like echo, newline mode."""
Chris Liechti4caf6a52015-08-04 01:07:45 +020056
cliechti80a0ed12003-10-03 23:53:42 +000057 def __init__(self, *args, **kwds):
58 self.settings = kwds['settings']
59 del kwds['settings']
60 # begin wxGlade: TerminalSettingsDialog.__init__
cliechtid5d51982008-04-10 23:48:55 +000061 kwds["style"] = wx.DEFAULT_DIALOG_STYLE
62 wx.Dialog.__init__(self, *args, **kwds)
63 self.checkbox_echo = wx.CheckBox(self, -1, "Local Echo")
64 self.checkbox_unprintable = wx.CheckBox(self, -1, "Show unprintable characters")
65 self.radio_box_newline = wx.RadioBox(self, -1, "Newline Handling", choices=["CR only", "LF only", "CR+LF"], majorDimension=0, style=wx.RA_SPECIFY_ROWS)
66 self.button_ok = wx.Button(self, -1, "OK")
67 self.button_cancel = wx.Button(self, -1, "Cancel")
cliechti80a0ed12003-10-03 23:53:42 +000068
69 self.__set_properties()
70 self.__do_layout()
71 # end wxGlade
72 self.__attach_events()
73 self.checkbox_echo.SetValue(self.settings.echo)
74 self.checkbox_unprintable.SetValue(self.settings.unprintable)
75 self.radio_box_newline.SetSelection(self.settings.newline)
76
77 def __set_properties(self):
78 # begin wxGlade: TerminalSettingsDialog.__set_properties
79 self.SetTitle("Terminal Settings")
80 self.radio_box_newline.SetSelection(0)
81 self.button_ok.SetDefault()
82 # end wxGlade
83
84 def __do_layout(self):
85 # begin wxGlade: TerminalSettingsDialog.__do_layout
cliechtid5d51982008-04-10 23:48:55 +000086 sizer_2 = wx.BoxSizer(wx.VERTICAL)
87 sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
88 sizer_4 = wx.StaticBoxSizer(wx.StaticBox(self, -1, "Input/Output"), wx.VERTICAL)
89 sizer_4.Add(self.checkbox_echo, 0, wx.ALL, 4)
90 sizer_4.Add(self.checkbox_unprintable, 0, wx.ALL, 4)
cliechti80a0ed12003-10-03 23:53:42 +000091 sizer_4.Add(self.radio_box_newline, 0, 0, 0)
cliechtid5d51982008-04-10 23:48:55 +000092 sizer_2.Add(sizer_4, 0, wx.EXPAND, 0)
cliechti80a0ed12003-10-03 23:53:42 +000093 sizer_3.Add(self.button_ok, 0, 0, 0)
94 sizer_3.Add(self.button_cancel, 0, 0, 0)
cliechtid5d51982008-04-10 23:48:55 +000095 sizer_2.Add(sizer_3, 0, wx.ALL|wx.ALIGN_RIGHT, 4)
cliechti80a0ed12003-10-03 23:53:42 +000096 self.SetAutoLayout(1)
97 self.SetSizer(sizer_2)
98 sizer_2.Fit(self)
99 sizer_2.SetSizeHints(self)
100 self.Layout()
101 # end wxGlade
102
103 def __attach_events(self):
cliechtid5d51982008-04-10 23:48:55 +0000104 self.Bind(wx.EVT_BUTTON, self.OnOK, id = self.button_ok.GetId())
105 self.Bind(wx.EVT_BUTTON, self.OnCancel, id = self.button_cancel.GetId())
Chris Liechti4caf6a52015-08-04 01:07:45 +0200106
cliechti80a0ed12003-10-03 23:53:42 +0000107 def OnOK(self, events):
cliechti0eb86712003-10-04 00:49:04 +0000108 """Update data wil new values and close dialog."""
cliechti80a0ed12003-10-03 23:53:42 +0000109 self.settings.echo = self.checkbox_echo.GetValue()
110 self.settings.unprintable = self.checkbox_unprintable.GetValue()
111 self.settings.newline = self.radio_box_newline.GetSelection()
cliechtid5d51982008-04-10 23:48:55 +0000112 self.EndModal(wx.ID_OK)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200113
cliechti80a0ed12003-10-03 23:53:42 +0000114 def OnCancel(self, events):
cliechti0eb86712003-10-04 00:49:04 +0000115 """Do not update data but close dialog."""
cliechtid5d51982008-04-10 23:48:55 +0000116 self.EndModal(wx.ID_CANCEL)
cliechti80a0ed12003-10-03 23:53:42 +0000117
118# end of class TerminalSettingsDialog
119
120
cliechtid5d51982008-04-10 23:48:55 +0000121class TerminalFrame(wx.Frame):
cliechti80a0ed12003-10-03 23:53:42 +0000122 """Simple terminal program for wxPython"""
Chris Liechti4caf6a52015-08-04 01:07:45 +0200123
cliechti80a0ed12003-10-03 23:53:42 +0000124 def __init__(self, *args, **kwds):
125 self.serial = serial.Serial()
cliechtibc318d42004-11-13 03:13:12 +0000126 self.serial.timeout = 0.5 #make sure that the alive event can be checked from time to time
cliechti0eb86712003-10-04 00:49:04 +0000127 self.settings = TerminalSetup() #placeholder for the settings
cliechti80a0ed12003-10-03 23:53:42 +0000128 self.thread = None
Chris Liechti4caf6a52015-08-04 01:07:45 +0200129 self.alive = threading.Event()
cliechti80a0ed12003-10-03 23:53:42 +0000130 # begin wxGlade: TerminalFrame.__init__
cliechtid5d51982008-04-10 23:48:55 +0000131 kwds["style"] = wx.DEFAULT_FRAME_STYLE
132 wx.Frame.__init__(self, *args, **kwds)
133 self.text_ctrl_output = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE|wx.TE_READONLY)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200134
cliechti80a0ed12003-10-03 23:53:42 +0000135 # Menu Bar
cliechtid5d51982008-04-10 23:48:55 +0000136 self.frame_terminal_menubar = wx.MenuBar()
cliechti80a0ed12003-10-03 23:53:42 +0000137 self.SetMenuBar(self.frame_terminal_menubar)
cliechtid5d51982008-04-10 23:48:55 +0000138 wxglade_tmp_menu = wx.Menu()
139 wxglade_tmp_menu.Append(ID_CLEAR, "&Clear", "", wx.ITEM_NORMAL)
140 wxglade_tmp_menu.Append(ID_SAVEAS, "&Save Text As...", "", wx.ITEM_NORMAL)
cliechti80a0ed12003-10-03 23:53:42 +0000141 wxglade_tmp_menu.AppendSeparator()
cliechtid5d51982008-04-10 23:48:55 +0000142 wxglade_tmp_menu.Append(ID_SETTINGS, "&Port Settings...", "", wx.ITEM_NORMAL)
143 wxglade_tmp_menu.Append(ID_TERM, "&Terminal Settings...", "", wx.ITEM_NORMAL)
cliechti80a0ed12003-10-03 23:53:42 +0000144 wxglade_tmp_menu.AppendSeparator()
cliechtid5d51982008-04-10 23:48:55 +0000145 wxglade_tmp_menu.Append(ID_EXIT, "&Exit", "", wx.ITEM_NORMAL)
cliechti80a0ed12003-10-03 23:53:42 +0000146 self.frame_terminal_menubar.Append(wxglade_tmp_menu, "&File")
147 # Menu Bar end
cliechti80a0ed12003-10-03 23:53:42 +0000148
149 self.__set_properties()
150 self.__do_layout()
151 # end wxGlade
cliechti0eb86712003-10-04 00:49:04 +0000152 self.__attach_events() #register events
cliechti80a0ed12003-10-03 23:53:42 +0000153 self.OnPortSettings(None) #call setup dialog on startup, opens port
cliechtibc318d42004-11-13 03:13:12 +0000154 if not self.alive.isSet():
cliechti0eb86712003-10-04 00:49:04 +0000155 self.Close()
cliechti80a0ed12003-10-03 23:53:42 +0000156
157 def StartThread(self):
cliechtibc318d42004-11-13 03:13:12 +0000158 """Start the receiver thread"""
cliechti80a0ed12003-10-03 23:53:42 +0000159 self.thread = threading.Thread(target=self.ComPortThread)
160 self.thread.setDaemon(1)
cliechtibc318d42004-11-13 03:13:12 +0000161 self.alive.set()
cliechtif7776692005-10-02 21:51:42 +0000162 self.thread.start()
cliechti80a0ed12003-10-03 23:53:42 +0000163
164 def StopThread(self):
cliechti0eb86712003-10-04 00:49:04 +0000165 """Stop the receiver thread, wait util it's finished."""
cliechti80a0ed12003-10-03 23:53:42 +0000166 if self.thread is not None:
cliechtibc318d42004-11-13 03:13:12 +0000167 self.alive.clear() #clear alive event for thread
cliechti80a0ed12003-10-03 23:53:42 +0000168 self.thread.join() #wait until thread has finished
169 self.thread = None
Chris Liechti4caf6a52015-08-04 01:07:45 +0200170
cliechti80a0ed12003-10-03 23:53:42 +0000171 def __set_properties(self):
172 # begin wxGlade: TerminalFrame.__set_properties
173 self.SetTitle("Serial Terminal")
174 self.SetSize((546, 383))
cliechti80a0ed12003-10-03 23:53:42 +0000175 # end wxGlade
176
177 def __do_layout(self):
178 # begin wxGlade: TerminalFrame.__do_layout
cliechtid5d51982008-04-10 23:48:55 +0000179 sizer_1 = wx.BoxSizer(wx.VERTICAL)
180 sizer_1.Add(self.text_ctrl_output, 1, wx.EXPAND, 0)
cliechti80a0ed12003-10-03 23:53:42 +0000181 self.SetAutoLayout(1)
182 self.SetSizer(sizer_1)
183 self.Layout()
184 # end wxGlade
185
186 def __attach_events(self):
cliechti0eb86712003-10-04 00:49:04 +0000187 #register events at the controls
cliechtid5d51982008-04-10 23:48:55 +0000188 self.Bind(wx.EVT_MENU, self.OnClear, id = ID_CLEAR)
189 self.Bind(wx.EVT_MENU, self.OnSaveAs, id = ID_SAVEAS)
190 self.Bind(wx.EVT_MENU, self.OnExit, id = ID_EXIT)
191 self.Bind(wx.EVT_MENU, self.OnPortSettings, id = ID_SETTINGS)
192 self.Bind(wx.EVT_MENU, self.OnTermSettings, id = ID_TERM)
193 self.text_ctrl_output.Bind(wx.EVT_CHAR, self.OnKey)
cliechtibc318d42004-11-13 03:13:12 +0000194 self.Bind(EVT_SERIALRX, self.OnSerialRead)
cliechtid5d51982008-04-10 23:48:55 +0000195 self.Bind(wx.EVT_CLOSE, self.OnClose)
cliechti80a0ed12003-10-03 23:53:42 +0000196
197 def OnExit(self, event):
198 """Menu point Exit"""
199 self.Close()
200
201 def OnClose(self, event):
cliechti0eb86712003-10-04 00:49:04 +0000202 """Called on application shutdown."""
cliechti80a0ed12003-10-03 23:53:42 +0000203 self.StopThread() #stop reader thread
204 self.serial.close() #cleanup
205 self.Destroy() #close windows, exit app
206
207 def OnSaveAs(self, event):
208 """Save contents of output window."""
209 filename = None
cliechtid5d51982008-04-10 23:48:55 +0000210 dlg = wx.FileDialog(None, "Save Text As...", ".", "", "Text File|*.txt|All Files|*", wx.SAVE)
211 if dlg.ShowModal() == wx.ID_OK:
cliechti80a0ed12003-10-03 23:53:42 +0000212 filename = dlg.GetPath()
213 dlg.Destroy()
214
215 if filename is not None:
216 f = file(filename, 'w')
217 text = self.text_ctrl_output.GetValue()
218 if type(text) == unicode:
219 text = text.encode("latin1") #hm, is that a good asumption?
220 f.write(text)
221 f.close()
222
223 def OnClear(self, event):
224 """Clear contents of output window."""
225 self.text_ctrl_output.Clear()
226
cliechti0eb86712003-10-04 00:49:04 +0000227 def OnPortSettings(self, event=None):
228 """Show the portsettings dialog. The reader thread is stopped for the
229 settings change."""
cliechtibc318d42004-11-13 03:13:12 +0000230 if event is not None: #will be none when called on startup
cliechti0eb86712003-10-04 00:49:04 +0000231 self.StopThread()
232 self.serial.close()
233 ok = False
234 while not ok:
235 dialog_serial_cfg = wxSerialConfigDialog.SerialConfigDialog(None, -1, "",
236 show=wxSerialConfigDialog.SHOW_BAUDRATE|wxSerialConfigDialog.SHOW_FORMAT|wxSerialConfigDialog.SHOW_FLOW,
237 serial=self.serial
238 )
239 result = dialog_serial_cfg.ShowModal()
240 dialog_serial_cfg.Destroy()
241 #open port if not called on startup, open it on startup and OK too
cliechtid5d51982008-04-10 23:48:55 +0000242 if result == wx.ID_OK or event is not None:
cliechti0eb86712003-10-04 00:49:04 +0000243 try:
244 self.serial.open()
Chris Liechti4caf6a52015-08-04 01:07:45 +0200245 except serial.SerialException as e:
cliechtid5d51982008-04-10 23:48:55 +0000246 dlg = wx.MessageDialog(None, str(e), "Serial Port Error", wx.OK | wx.ICON_ERROR)
cliechti0eb86712003-10-04 00:49:04 +0000247 dlg.ShowModal()
248 dlg.Destroy()
249 else:
250 self.StartThread()
251 self.SetTitle("Serial Terminal on %s [%s, %s%s%s%s%s]" % (
252 self.serial.portstr,
253 self.serial.baudrate,
254 self.serial.bytesize,
255 self.serial.parity,
256 self.serial.stopbits,
257 self.serial.rtscts and ' RTS/CTS' or '',
258 self.serial.xonxoff and ' Xon/Xoff' or '',
259 )
260 )
261 ok = True
262 else:
263 #on startup, dialog aborted
cliechtibc318d42004-11-13 03:13:12 +0000264 self.alive.clear()
cliechti0eb86712003-10-04 00:49:04 +0000265 ok = True
cliechti80a0ed12003-10-03 23:53:42 +0000266
267 def OnTermSettings(self, event):
268 """Menu point Terminal Settings. Show the settings dialog
269 with the current terminal settings"""
270 dialog = TerminalSettingsDialog(None, -1, "", settings=self.settings)
271 result = dialog.ShowModal()
272 dialog.Destroy()
Chris Liechti4caf6a52015-08-04 01:07:45 +0200273
cliechti80a0ed12003-10-03 23:53:42 +0000274 def OnKey(self, event):
275 """Key event handler. if the key is in the ASCII range, write it to the serial port.
276 Newline handling and local echo is also done here."""
277 code = event.GetKeyCode()
278 if code < 256: #is it printable?
279 if code == 13: #is it a newline? (check for CR which is the RETURN key)
280 if self.settings.echo: #do echo if needed
281 self.text_ctrl_output.AppendText('\n')
282 if self.settings.newline == NEWLINE_CR:
283 self.serial.write('\r') #send CR
284 elif self.settings.newline == NEWLINE_LF:
285 self.serial.write('\n') #send LF
286 elif self.settings.newline == NEWLINE_CRLF:
287 self.serial.write('\r\n') #send CR+LF
288 else:
289 char = chr(code)
290 if self.settings.echo: #do echo if needed
291 self.text_ctrl_output.WriteText(char)
292 self.serial.write(char) #send the charcater
cliechti0eb86712003-10-04 00:49:04 +0000293 else:
Chris Liechti4caf6a52015-08-04 01:07:45 +0200294 print("Extra Key:", code)
cliechti80a0ed12003-10-03 23:53:42 +0000295
cliechti40d71f62004-07-09 22:14:17 +0000296 def OnSerialRead(self, event):
cliechti0eb86712003-10-04 00:49:04 +0000297 """Handle input from the serial port."""
cliechti40d71f62004-07-09 22:14:17 +0000298 text = event.data
cliechti80a0ed12003-10-03 23:53:42 +0000299 if self.settings.unprintable:
300 text = ''.join([(c >= ' ') and c or '<%d>' % ord(c) for c in text])
301 self.text_ctrl_output.AppendText(text)
302
303 def ComPortThread(self):
cliechti0eb86712003-10-04 00:49:04 +0000304 """Thread that handles the incomming traffic. Does the basic input
cliechti40d71f62004-07-09 22:14:17 +0000305 transformation (newlines) and generates an SerialRxEvent"""
cliechtibc318d42004-11-13 03:13:12 +0000306 while self.alive.isSet(): #loop while alive event is true
cliechti80a0ed12003-10-03 23:53:42 +0000307 text = self.serial.read(1) #read one, with timout
308 if text: #check if not timeout
309 n = self.serial.inWaiting() #look if there is more to read
310 if n:
311 text = text + self.serial.read(n) #get it
312 #newline transformation
313 if self.settings.newline == NEWLINE_CR:
314 text = text.replace('\r', '\n')
315 elif self.settings.newline == NEWLINE_LF:
316 pass
317 elif self.settings.newline == NEWLINE_CRLF:
318 text = text.replace('\r\n', '\n')
cliechti40d71f62004-07-09 22:14:17 +0000319 event = SerialRxEvent(self.GetId(), text)
320 self.GetEventHandler().AddPendingEvent(event)
321 #~ self.OnSerialRead(text) #output text in window
Chris Liechti4caf6a52015-08-04 01:07:45 +0200322
cliechti80a0ed12003-10-03 23:53:42 +0000323# end of class TerminalFrame
324
325
cliechtid5d51982008-04-10 23:48:55 +0000326class MyApp(wx.App):
cliechti80a0ed12003-10-03 23:53:42 +0000327 def OnInit(self):
cliechtid5d51982008-04-10 23:48:55 +0000328 wx.InitAllImageHandlers()
cliechti80a0ed12003-10-03 23:53:42 +0000329 frame_terminal = TerminalFrame(None, -1, "")
330 self.SetTopWindow(frame_terminal)
331 frame_terminal.Show(1)
332 return 1
333
334# end of class MyApp
335
336if __name__ == "__main__":
337 app = MyApp(0)
338 app.MainLoop()