class FdaAltimeterControl(tk.Toplevel): PORT_SELECTION_FRAME_HEIGHT = 80 SUB_FRAME_X_PADDING = 20 def __init__(self, parent): tk.Toplevel.__init__(self) self.parent = parent # Do not let user to close this window # is a communication with the Altimeter # is taking place. self.communicating = False self.protocol("WM_DELETE_WINDOW", self.close_asked) # Setup window content. self.initialize() def initialize(self): self.title(_(u'FlyDream Altimeter - Device Controller')) # Set fixed size. self.minsize(640, 320) self.resizable(False, False) # Altimeter local device. frame = tk.Frame(self, height=self.PORT_SELECTION_FRAME_HEIGHT) frame.pack(fill=tk.BOTH, expand=tk.YES) tk.Label(frame, text=_(u'Altimeter plugged on:')).pack(side=tk.LEFT) self.detect = tk.Button(frame, text=_(u'Refresh'), command=self.refresh_serial_ports) self.detect.pack(side=tk.RIGHT) self.port = tk.StringVar(self) self.ports = tk.OptionMenu(frame, self.port, _(u'Detecting serial ports...')) self.ports.pack(fill=tk.X, expand=tk.YES) # Update possible serial ports. self.refresh_serial_ports() # Upload altimeter flight data. frame = tk.Frame(self, padx=self.SUB_FRAME_X_PADDING) frame.pack(fill=tk.BOTH, expand=tk.YES) label = tk.Label(frame, anchor=tk.W, text=_(u'Upload')) label.pack(side=tk.TOP, fill=tk.X) # Setup bold font for titles. f = Font(font=label['font']) f['weight'] = 'bold' label['font'] = f.name Separator(frame).pack(fill=tk.X) self.progressbar = Progressbar(frame, orient='horizontal', mode='determinate') self.progressbar.pack(side=tk.BOTTOM, fill=tk.X) # Do not show progressbar # unless data is uploaded. self.hide_progressbar() self.upload_info = tk.StringVar() self.upload_info.set('/') self.info_label = tk.Label(frame, anchor=tk.NE, fg='darkgrey', textvariable=self.upload_info) self.info_label.pack(side=tk.BOTTOM, fill=tk.BOTH) self.label = tk.Label(frame, anchor=tk.W, text=_(u'Tell the altimeter to send flight ' 'data to your computer')) self.label.pack(side=tk.LEFT) self.upload = tk.Button(frame, text=_(u'Upload data'), command=self.upload) self.upload.pack(side=tk.RIGHT) # Erase altimeter flight data. frame = tk.Frame(self, padx=self.SUB_FRAME_X_PADDING) frame.pack(fill=tk.BOTH, expand=tk.YES) label = tk.Label(frame, anchor=tk.W, text=_(u'Clear')) label['font'] = f.name label.pack(side=tk.TOP, fill=tk.X) Separator(frame).pack(fill=tk.X) label = tk.Label(frame, anchor=tk.W, text=_(u'Delete all the flight data from your altimeter')) label.pack(side=tk.LEFT) self.erase = tk.Button(frame, text=_(u'Erase data'), command=self.erase) self.erase.pack(side=tk.RIGHT) # Setup altimeter sampling frequency. frame = tk.Frame(self, padx=self.SUB_FRAME_X_PADDING) frame.pack(fill=tk.BOTH, expand=tk.YES) label = tk.Label(frame, anchor=tk.W, text=_(u'Configure')) label['font'] = f.name label.pack(side=tk.TOP, fill=tk.X) Separator(frame).pack(fill=tk.X) self.frequency = tk.StringVar(self) self.frequency.set('1') self.frequencies = tk.OptionMenu(frame, self.frequency, '1', '2', '4', '8') self.frequencies.pack(side=tk.LEFT) label = tk.Label(frame, text=_(u'records per second')) label.pack(side=tk.LEFT) self.setup = tk.Button(frame, text=_(u'Set sampling frequency'), command=self.set_frequency) self.setup.pack(side=tk.RIGHT) def allow_user_interactions(self, allow=True): state = tk.NORMAL if allow else tk.DISABLED self.ports.configure(state=state) self.detect.configure(state=state) self.upload.configure(state=state) self.erase.configure(state=state) self.frequencies.configure(state=state) self.setup.configure(state=state) def close_asked(self): if not self.communicating: self.destroy() return else: showwarning(_(u'Warning'), _(u"""The application is communicating with the altimeter. Please wait until communication is finished before closing this window.""")) def hide_progressbar(self): self.progressbar.prev_pack = self.progressbar.pack_info() self.progressbar.pack_forget() self.update() # Reset progressbar for next upload. self.progressbar['value'] = 0 def show_progressbar(self): # Well, Tkinter pack does not seem to # make it easy to simply hide a widget... # # I may be missing something. prev_info_label_packinfo = self.info_label.pack_info() prev_label_packinfo = self.label.pack_info() prev_upload_packinfo = self.upload.pack_info() self.info_label.pack_forget() self.label.pack_forget() self.upload.pack_forget() self.progressbar.pack(self.progressbar.prev_pack) self.info_label.pack(prev_info_label_packinfo) self.label.pack(prev_label_packinfo) self.upload.pack(prev_upload_packinfo) self.update() def upload_progressed(self, read, total): self.progressbar['maximum'] = total self.progressbar['value'] = read info = _(u'Please wait, %d bytes read out of %d') % (read, total) self.upload_info.set(info) self.update() def default_filename(self): return time.strftime('%Y-%m-%d %H-%M-%S', time.localtime()) \ + '_flight' def upload(self): # TODO Add please wait message. # Update window state. self.show_progressbar() self.allow_user_interactions(False) self.communicating = True # Get flight data. port = self.port.get() altimeter = Altimeter(port) try: raw_data = altimeter.upload(self.upload_progressed) except FlyDreamAltimeterSerialPortError: self.show_unfound_altimeter(port) except (FlyDreamAltimeterReadError, FlyDreamAltimeterWriteError) as e: self.show_readwrite_error(port, e.message) except FlyDreamAltimeterProtocolError as e: self.show_protocol_error(port, e.message) else: # Check received data. if len(raw_data.data) == 0: showinfo(_(u'Upload Data'), _(u'Altimeter contains no data.')) else: filename = self.write_flight_data(raw_data) self.suggest_open_in_viewer(filename) finally: # Restore window state. self.communicating = False self.allow_user_interactions() self.hide_progressbar() # TODO Update altimeter information. self.upload_info.set(_(u'Done')) def write_flight_data(self, raw_data): # Let user choose a file name. # # When cancelled, None is returned. fname = asksaveasfilename( filetypes=((_(u'Flydream Altimeter Data'), '*.fda'), (_(u'All files'), '*.*')), title=_(u'Save flight data...'), initialdir='~', initialfile=self.default_filename(), defaultextension=RAW_FILE_EXTENSION) if fname: # Try to remove previous file. # # User has been warned by asksaveasfilename # if she picked an existing file name. try: os.remove(fname) except OSError as e: # errno.ENOENT is 'no such file or directory'. # # Raise if another kind of error occurred. if e.errno != errno.ENOENT: raise # This method raises an exception if file already exists. raw_data.to_file(fname) return fname def suggest_open_in_viewer(self, filename): # User may want to see latest uploaded data. if filename: reply = askokcancel(_(u'Open in viewer'), _(u'Open saved flight data in file viewer?')) if reply: self.parent.load_file(filename) def erase(self): reply = askokcancel(_(u'Erase flight data'), _(u'Really erase all flight data in your altimeter?')) if reply: self.do_erase() def do_erase(self): # TODO Make message stay while erasing is performed. showwarning(_(u'Erasing...'), _(u'Do not disconnect USB until altimeter blue LED ' 'lights again.')) # Update window state. self.allow_user_interactions(False) self.communicating = True # Reset altimeter content. port = self.port.get() altimeter = Altimeter(port) try: altimeter.clear() except FlyDreamAltimeterSerialPortError: self.show_unfound_altimeter(port) except (FlyDreamAltimeterReadError, FlyDreamAltimeterWriteError) as e: self.show_readwrite_error(port, e.message) except FlyDreamAltimeterProtocolError as e: self.show_protocol_error(port, e.message) else: # Update altimeter information. self.upload_info.set(_(u'Altimeter content erased')) finally: # Restore window state. self.communicating = False self.allow_user_interactions() def set_frequency(self): # This request is almost immediate, # so let's not bother user with yet # another message box. # Update window state. self.allow_user_interactions(False) self.communicating = True # Change altimeter sampling frequency. freq = int(self.frequency.get()) port = self.port.get() altimeter = Altimeter(port) try: altimeter.setup(freq) except FlyDreamAltimeterSerialPortError: self.show_unfound_altimeter(port) except (FlyDreamAltimeterReadError, FlyDreamAltimeterWriteError) as e: self.show_readwrite_error(port, e.message) except FlyDreamAltimeterProtocolError as e: self.show_protocol_error(port, e.message) else: # Update altimeter information. self.upload_info.set(_(u'Altimeter sampling frequency set')) finally: # Restore window state. self.communicating = False self.allow_user_interactions() def show_unfound_altimeter(self, port): showwarning(_(u'Sampling Frequency'), _(u"""Can not open port: %s Please ensure that: - your altimeter is plugged to the USB adapter. - the USB adapter is plugged to your computer. - the choosen port is correct. - the USB adapter driver is properly installed on your computer, see http://www.silabs.com/products/mcu/pages/usbtouartbridgevcpdrivers.aspx""") % port) def show_readwrite_error(self, port, message): showwarning(_(u'Read/Write error'), _(u"""With device on: %s Internal error message: %s Please ensure that: - the choosen port is correct. - your altimeter is plugged to the USB adapter. - the USB adapter is plugged to your computer. - you did not unplugged the altimeter while it was communicating.""") % (port, message)) def show_protocol_error(self, port, message): showwarning(_(u'Protocol error'), _(u"""With device on: %s Internal error message: %s Please ensure that: - your altimeter is plugged to the USB adapter. - you did not unplugged the altimeter while it was communicating.""") % (port, message)) def refresh_serial_ports(self): # Build port list. port_list = self.detect_serial_ports() # Remove previous items in combobox. menu = self.ports['menu'] menu.delete(0, tk.END) # Add new items in combobox. self.port.set(port_list[0]) for pr in port_list: menu.add_command(label=pr, command=lambda p=pr: self.port.set(p)) def detect_serial_ports(self): # Try to detect system serial devices. port_list = [] try: # Convert to filenames. port_list = [info[0].replace('cu', 'tty') for info in list_ports.comports()] except Exception as e: showwarning(_(u'Serial Ports Detection'), _(u'Error while detecting serial ports: %s') % e.message) # Insert default port as first entry. default = Altimeter().port try: # Try to remove default one to prevent duplication. port_list.remove(default) except ValueError: pass port_list.insert(0, default) return port_list