class Playbar(Frame): def __init__(self, parent=None): Frame.__init__(self, parent) self.pack(side=BOTTOM, anchor=S, fill=X) self.scale = Scale(self, from_=0, to=1, sliderlength = 10, showvalue=YES, tickinterval=10, orient='horizontal') self.scale.pack(fill=X) # We don't set the initial value of the scale here because # that's done by tying it to the global variable "slidert" # Resetscale: takes a frame count and resets scale parms appropriately def resetscale(self, frames): temp = floor(log10(frames)) tickinterval = int(pow(10,temp)) if(tickinterval<1): tickinterval = 1 self.scale.config(to=frames, tickinterval=tickinterval)
class Playbar(Frame): def __init__(self, parent=None): Frame.__init__(self, parent) self.pack(side=BOTTOM, anchor=S, fill=X) self.scale = Scale(self, from_=0, to=1, sliderlength=10, showvalue=YES, tickinterval=10, orient='horizontal') self.scale.pack(fill=X) # We don't set the initial value of the scale here because # that's done by tying it to the global variable "slidert" # Resetscale: takes a frame count and resets scale parms appropriately def resetscale(self, frames): temp = floor(log10(frames)) tickinterval = int(pow(10, temp)) if (tickinterval < 1): tickinterval = 1 self.scale.config(to=frames, tickinterval=tickinterval)
class Visualiser(object): ''' Generic Offline Visualiser. Subclasses need to be used that specify how to handle a data source. ''' ### Public functions ### def __init__(self, title="Visualisation", width=400, height=400, recording=False, recordPattern=None, paused=False, source=None): ''' Constructor. Params: title: string - Title of the visualisation window width: int - Width of the visualisation window height: int - Height of the visualisation window recording: boolean - Start with recording enabled? recordPattern: string - Pattern for recorded images, e.g., cylinders%05g.png paused: boolean - Start with playback paused? source:- The data source to read. What is required here varies by visualiser. ''' # Visualisation options self.vis_features = [] self.vis_frame = 0 self.vis_frameStep = 1 self.vis_jumping = False self.vis_recording = recording self.vis_recordPattern = recordPattern self.vis_paused = paused self.vis_source = source # VTK structures self.vtk_cells = vtkCellArray() self.vtk_renderer = vtkRenderer() # Tk structures self.tk_root = Tk() self.tk_root.title(title) self.tk_root.grid_rowconfigure(0, weight=1) self.tk_root.grid_columnconfigure(0, weight=3) self.tk_root.bind('<Destroy>', self.destroyed) if not self.vis_paused: self.tk_root.after(100, self.animate) self.tk_renderWidget = vtkTkRenderWidget(self.tk_root, width=width, height=height) self.tk_renderWidget.grid(row=0, column=0, sticky=N+S+E+W) self.tk_renderWidget.GetRenderWindow().AddRenderer(self.vtk_renderer) self.tk_featureFrame = Frame(self.tk_root) self.tk_featureFrame.grid(row=0, column=1, rowspan=2) Label(self.tk_featureFrame, text='Features:').grid(row=0, column=0) self.tk_controlFrame = Frame(self.tk_root) self.tk_controlFrame.grid(row=1, column=0) self.tk_quit = Button(self.tk_controlFrame, text="Quit", command=self.shutdown) self.tk_quit.grid(row=0, column=0, columnspan=2) def pause(): if self.vis_paused: self.tk_pause.config(text='Pause') self.tk_root.after(100, self.animate) else: self.tk_pause.config(text='Resume') self.vis_paused ^= True self.tk_pause = Button(self.tk_controlFrame, text="Pause", command=pause) self.tk_pause.grid(row=0, column=2, columnspan=2) if self.vis_recordPattern is not None: def record(): if self.vis_recording: self.tk_record.config(text='Start Recording') else: self.tk_record.config(text='Stop Recording') self.vis_recording ^= True self.tk_record = Button(self.tk_controlFrame, text="Start Recording", command=record) self.tk_record.grid(row=0, column=4, columnspan=2) if self.vis_recording: self.tk_record.config(text="Stop Recording") def make_seek_button(label, column, frame): def jump(): self.jumpTo(frame) b = Button(self.tk_controlFrame, text=label, command=jump) b.grid(row=1, column=column, sticky=W+E) return b self.tk_seek_start = make_seek_button("|<", 0, 0) self.tk_seek_back10 = make_seek_button("<<", 1, lambda: self.vis_frame - 10) self.tk_seek_back1 = make_seek_button("<", 2, lambda: self.vis_frame - 1) self.tk_seek_forward1 = make_seek_button(">", 3, lambda: self.vis_frame + 1) self.tk_seek_forward10 = make_seek_button(">>", 4, lambda: self.vis_frame + 10) self.tk_seek_end = make_seek_button(">|", 5, self.getMaxFrameNumber) Label(self.tk_controlFrame, text='Frame').grid(row=2, column=0, sticky=W+E) def changeFrame(frame): if not self.vis_jumping: self.vis_jumping = True self.jumpTo(self.tk_frame.get()) self.vis_jumping = False self.tk_frame = Scale(self.tk_controlFrame, command=changeFrame, from_=0, to=0, orient=HORIZONTAL) self.tk_frame.grid(row=2, column=1, columnspan=2, sticky=W+E) Label(self.tk_controlFrame, text='Step').grid(row=2, column=3, sticky=W+E) def changeFrameStep(step): self.vis_frameStep = int(step) self.tk_frameStep = Scale(self.tk_controlFrame, command=changeFrameStep, from_=1, to=1, orient=HORIZONTAL) self.tk_frameStep.grid(row=2, column=4, columnspan=2, sticky=W+E) self.setupGrid() def add_feature(self, feature): '''Add a feature to this visualiser''' self.vis_features.append(feature) feature.button(self.tk_featureFrame).grid(row=len(self.vis_features), column=0, sticky=W+E) feature.visualiser = self def run(self): '''Start the visualiser''' self.redraw() self.tk_root.mainloop() ### Private funcitions ### def resetSliders(self): ''' Recalculate the upper bound on the frame and frameStep sliders. ''' maxFrame = self.getMaxFrameNumber() self.tk_frame.config(to=maxFrame) self.tk_frameStep.config(to=maxFrame) def jumpTo(self, frame): ''' Jump to a given frame. If frame is a function, jump to the return value of frame(). ''' oldFrame = self.vis_frame if hasattr(frame, '__call__'): self.vis_frame = frame() else: self.vis_frame = frame maxFrame = self.getMaxFrameNumber() if self.vis_frame < 0: self.vis_frame = 0 elif self.vis_frame > maxFrame: self.vis_frame = maxFrame self.vis_paused = True self.tk_pause.config(text='Resume') self.tk_frame.set(self.vis_frame) self.redraw(oldFrame != self.vis_frame) def redraw(self, update=False): self.resetSliders() for feature in [ f for f in self.vis_features if not f.dynamic ]: feature.draw(self.vtk_renderer) if update: for feature in [ f for f in self.vis_features if f.dynamic]: f.redraw(self.vtk_renderer) self.tk_renderWidget.GetRenderWindow().Render() self.tk_root.update_idletasks() ### Gui events ### def destroyed(self, event): if event.widget == self.tk_root: self.shutdown() def shutdown(self): self.tk_root.withdraw() self.tk_root.destroy() def animate(self): if not self.vis_paused: self.jumpTo(self.vis_frame + self.vis_frameStep) if self.vis_recording and self.vis_recordPattern is not None: self.save_image() self.tk_root.after(100, self.animate) def save_image(self): extmap = {'.jpg' : vtkJPEGWriter, '.jpeg' : vtkJPEGWriter, '.png' : vtkPNGWriter, '.pnm' : vtkPNMWriter} _, ext = splitext(self.vis_recordPattern) try: writer = extmap[ext.lower()]() except KeyError: print 'ERROR: Can\'t handle %s extension. Recording disabled.' % ext self.vis_recordPattern = None return win = self.vtk_renderer.GetRenderWindow() w2i = vtkWindowToImageFilter() w2i.SetInput(win) w2i.Update() writer.SetInput(w2i.GetOutput()) writer.SetFileName(self.vis_recordPattern % self.vis_frame) win.Render() writer.Write() ### Things subclasses need to override ### def setupGrid(self): ''' Populate the vtkCellArray instance at self.vtk_cells. Subclasses are required to override this function to read from their source as appropriate. ''' raise NotImplementedError('Subclass needs to override Visualiser::setupGrid!') def getMaxFrameNumber(self): ''' Return the maximum frame number. This will need to be defined by a subclass. ''' raise NotImplementedError('Subclass needs to override Visualiser::getMaxFrameNumber!') def getQuantityPoints(self, quantityName, dynamic=True, frameNumber=0): ''' Return the points of a quantity at a given frame as a list [float]. Subclasses need to override this. ''' raise NotImplementedError('Subclass needs to override Visualiser::getQuantityPoints!') def getQuantityDict(self): ''' Return the values of all quantities at a given time as a dictionary. Sublclasses need to override this. ''' raise NotImplementedError('Subclass needs to override Visualiser::getQuantityDict!')
class DPSinterface: """ DSPinterface is a Tk graphical interface to drive a DPS power supplier. """ def __init__(self, root): """ Create a DSP interface instance. :param root: is the Tk() interface where the DPS will be drawedstring with the prot name i.e. /dev/ttyUSB0 or COM5 for Windows :returns: a new instance of DPS graphical interface """ self.root = root root.title("DPS power supplier interface") root.protocol("WM_DELETE_WINDOW", self.wnwcmdclose) self.dps = None self.poller = None self.waver = None self.strtme = time() self.dpsfwave = None self.maxoutv = 5 self.maxoutc = 5 menubar = Menu(root) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label="Exit", command=self.wnwcmdclose) menubar.add_cascade(label="File", menu=filemenu) scopemenu = Menu(menubar, tearoff=0) scopemenu.add_command(label="Load sampled points...", command=self.mnucmdloadsmppts) scopemenu.add_command(label="Save sampled points as...", command=self.mnucmdsavesmppts) menubar.add_cascade(label="Scope", menu=scopemenu) wavemenu = Menu(menubar, tearoff=0) wavemenu.add_command(label="New wave", command=self.mnucmdnewwve) wavemenu.add_command(label="Load wave...", command=self.mnucmdloadwve) wavemenu.add_command(label="Edit wave...", command=self.mnucmdedtwve) wavemenu.add_command(label="Save wave as...", command=self.mnucmdsavewve) menubar.add_cascade(label="Wave", menu=wavemenu) memmenu = Menu(menubar, tearoff=0) memmenu.add_command(label="Edit memories...", command=self.mnucmdedtmem) menubar.add_cascade(label="Memory", menu=memmenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label="Help...", command=self.mnucmdhelp) helpmenu.add_command(label="About...", command=self.mnucmdabout) menubar.add_cascade(label="Help", menu=helpmenu) root.config(menu=menubar) row = 0 col = 0 rowspan = 1 colspan = 1 insertlabelrow(root, row, col, ("Serial: ", None, "Addr,Baud: "), E) col += colspan self.svardpsport = StringVar() self.svardpsport.set('/dev/ttyUSB0') self.entryserport = Entry(root, textvariable=self.svardpsport, width=ENTRYWIDTH, justify='right') self.entryserport.grid(row=row, column=col, sticky=W) col += colspan col += colspan self.svardpsaddbrt = StringVar() self.svardpsaddbrt.set('1, 9600') self.entrydpsadd = Entry(root, textvariable=self.svardpsaddbrt, width=ENTRYWIDTH, justify='right') self.entrydpsadd.grid(row=row, column=col, sticky=W) col += colspan colspan = 2 self.ivarconctd = IntVar() self.ivarconctd.set(0) Checkbutton(root, variable=self.ivarconctd, text='Connect', command=self.butcmdconnect).grid(row=row, column=col, columnspan=colspan, sticky=E + W) row += rowspan col = 0 colspan = 1 Separator(root, orient='horizontal').grid(row=row, columnspan=8, sticky=E + W, pady=8) row += rowspan rowspan = 1 colspan = 2 col = 0 self.ivarbrghtnes = IntVar() s = Scale(root, label='Brightness', variable=self.ivarbrghtnes, from_=0, to=5, resolution=1, orient="horizontal") s.bind("<ButtonRelease-1>", self.sclbndbrghtnss) s.grid(row=row, column=col, columnspan=colspan, sticky=E + W) col += colspan colspan = 1 Label(root, text="Model: ").grid(row=row, column=col, sticky=E) col += colspan self.ivarmodel = IntVar() Entry(root, textvariable=self.ivarmodel, state="readonly", width=ENTRYWIDTH, justify='right').grid(row=row, column=col, sticky=W) col += colspan colspan = 2 self.ivarsetmem = IntVar() s = Scale(root, label='Mem Recall', variable=self.ivarsetmem, from_=1, to=9, resolution=1, orient="horizontal") s.bind("<ButtonRelease-1>", self.sclbndmemory) s.grid(row=row, column=col, columnspan=colspan, sticky=E + W) row += rowspan colspan = 1 col = 0 insertlabelrow( root, row, col, (("Vinp [V]: ", VCOL), None, "Out Mode: ", None, "Protection: "), E) self.dvarvinp = DoubleVar() self.svarwrmde = StringVar() self.setworkmode(0) self.svarprot = StringVar() self.setprotection(0) insertentryrow( root, row, col, (None, self.dvarvinp, None, self.svarwrmde, None, self.svarprot), 'right', W, 'readonly') colspan = 1 row += rowspan col = 0 insertlabelrow(root, row, col, (("Vmax [V]: ", VCOL), None, ("Cmax [A]: ", CCOL), None, ("Pmax [W]: ", PCOL)), E) self.dvarvmaxm0 = DoubleVar() self.dvarcmaxm0 = DoubleVar() self.dvarpmaxm0 = DoubleVar() entries = insertentryrow(root, row, col, (None, self.dvarvmaxm0, None, self.dvarcmaxm0, None, self.dvarpmaxm0), 'right', W) for e, f in zip(entries, (self.entbndvmax, self.entbndcmax, self.entbndpmax)): e.bind('<FocusOut>', f) e.bind('<Return>', f) row += rowspan col = 0 insertlabelrow(root, row, col, (("Vout [V]: ", VCOL), None, ("Cout [A]: ", CCOL), None, ("Pout [W]: ", PCOL)), E) self.dvarvout = DoubleVar() self.dvarcout = DoubleVar() self.dvarpout = DoubleVar() insertentryrow( root, row, col, (None, self.dvarvout, None, self.dvarcout, None, self.dvarpout), 'right', W, 'readonly') row += rowspan col = 0 self.scope = Scope(root, [], row, col) row += 9 col = 4 Label(root, text="Rte[s/Sa]: ").grid(row=row, column=col, sticky=E) col += colspan self.dvarsecsmp = DoubleVar() self.dvarsecsmp.set(self.scope.sampletime()) e = Entry(root, textvariable=self.dvarsecsmp, width=ENTRYWIDTH, justify='right').grid(row=row, column=col, sticky=W) row += rowspan col = 0 colspan = 2 self.ivaracquire = IntVar() self.ivaracquire.set(0) Checkbutton(root, variable=self.ivaracquire, text='Run Acquisition', command=self.butcmdacquire).grid(row=row, column=col, columnspan=2, sticky=E + W) col += colspan self.ivarkeylock = IntVar() self.ivarkeylock.set(0) Checkbutton(root, variable=self.ivarkeylock, text="Key Lock", command=self.butcmdkeylock).grid(row=row, column=col, sticky=E + W, columnspan=colspan) col += colspan self.ivaroutenab = IntVar() self.ivaroutenab.set(0) Checkbutton(root, variable=self.ivaroutenab, text="Output Enable", command=self.butcmdoutenable).grid(row=row, column=col, sticky=E + W, columnspan=colspan) row += rowspan col = 0 rowspan = 1 colspan = 3 self.dvarvscale = DoubleVar() self.voltscale = Scale(root, label='Vset [V]', foreground=VCOL, variable=self.dvarvscale, from_=0, to=self.maxoutv, resolution=1, orient="horizontal") #, label='Vset[V]' self.voltscale.bind("<ButtonRelease-1>", self.sclbndvolt) self.voltscale.grid(row=row, column=col, columnspan=colspan, sticky=E + W) col += colspan self.dvarcscale = DoubleVar() self.curntscale = Scale(root, label='Cset[A]', foreground=CCOL, variable=self.dvarcscale, from_=0, to=self.maxoutc, resolution=1, orient="horizontal") #,label='Cset[A]' self.curntscale.bind("<ButtonRelease-1>", self.sclbndcrnt) self.curntscale.grid(row=row, column=col, columnspan=colspan, sticky=E + W) row += rowspan col = 0 self.dvarvscalef = DoubleVar() sc = Scale(root, foreground=VCOL, variable=self.dvarvscalef, from_=0, to=0.99, resolution=0.01, orient="horizontal") sc.bind("<ButtonRelease-1>", self.sclbndvolt) sc.grid(row=row, column=col, columnspan=colspan, sticky=E + W) col += colspan self.dvarcscalef = DoubleVar() sc = Scale(root, foreground=CCOL, variable=self.dvarcscalef, from_=0, to=0.99, resolution=0.01, orient="horizontal") sc.bind("<ButtonRelease-1>", self.sclbndcrnt) sc.grid(row=row, column=col, columnspan=colspan, sticky=E + W) row += rowspan col = 0 colspan = 1 Separator(root, orient='horizontal').grid(row=row, columnspan=6, sticky=E + W, pady=8) row += rowspan colspan = 1 col = 0 Label(root, text="Waveform: ").grid(row=row, column=col, sticky=E) col += colspan colspan = 2 self.svarwave = StringVar() Entry(root, textvariable=self.svarwave, width=ENTRYWIDTH, justify='right', state='readonly').grid(row=row, column=col, columnspan=colspan, sticky=E + W) col += colspan colspan = 1 self.ivarplaywv = IntVar() self.ivarplaywv.set(0) Checkbutton(root, variable=self.ivarplaywv, text='Play', command=self.butcmdplaywave).grid(row=row, column=col, sticky=E + W) col += colspan self.ivarpausewv = IntVar() self.ivarpausewv.set(0) Checkbutton(root, variable=self.ivarpausewv, text='Pause', command=self.butcmdpausewave).grid(row=row, column=col, sticky=E + W) col += colspan self.ivarloopwv = IntVar() self.ivarloopwv.set(0) Checkbutton(root, variable=self.ivarloopwv, text='Loop').grid(row=row, column=col, sticky=E + W) self.scope.update() self.scope.redraw() def sclbndvolt(self, event): """ Voltage scale bind command to set the voltage on the DSP. :param event: the event describing what changed """ if self.isconnected(): self.dps.set(['vset'], [self.getvscale()]) def sclbndcrnt(self, event): """ Current scale bind command to set the current on the DSP. :param event: the event describing what changed """ if self.isconnected(): self.dps.set(['cset'], [self.getcscale()]) def sclbndmemory(self, event): """ Memory-set bind command to set the memory on the DSP. :param event: the event describing what changed """ if self.isconnected(): m = self.ivarsetmem.get() self.dps.set(['mset'], [m]) self.updatefields(True) def sclbndbrghtnss(self, event): """ Brightness bind command to set the brightness on the DSP. :param event: the event describing what changed """ if self.isconnected(): b = self.ivarbrghtnes.get() self.dps.set(['brght'], [b]) def mnucmdnewwve(self): """ New wave menu command to initialize a new wave. """ self.dpsfwave = Dpsfile() self.svarwave.set('unnamed') def mnucmdloadwve(self): """ Load wave menu command to load a wave file. """ fname = tkFileDialog.askopenfilename(initialdir=".", title="Select wave file to load", filetypes=(("dps files", "*.dps"), ("all files", "*.*"))) if fname: self.svarwave.set(os.path.basename(fname)) self.dpsfwave = Dpsfile() self.dpsfwave.load(fname) def mnucmdedtwve(self): """ Edit wave menu command to open the edit wave window. """ if self.dpsfwave is not None: Wveinterface(self.root, self.dpsfwave.getpoints()) else: tkMessageBox.showinfo('No wave loaded', 'Load or create a new wave file to modify') def mnucmdsavewve(self): """ Save wave menu command to save the current wave in memory. """ if self.dpsfwave is not None: fname = tkFileDialog.asksaveasfilename( initialdir=".", title="Select wave file to save", filetypes=(("dps files", "*.dps"), ("all files", "*.*"))) if fname: self.dpsfwave.save(fname) self.svarwave.set(os.path.basename(fname)) else: tkMessageBox.showinfo('No wave in memory', 'Load or create a wave file to modify') def mnucmdloadsmppts(self): """ Load sampled points menu command to load in the graphical view sampled before. """ fname = tkFileDialog.askopenfilename(initialdir=".", title="Select data file to load", filetypes=(("dps files", "*.dps"), ("all files", "*.*"))) if fname: self.scope.load(fname) def mnucmdsavesmppts(self): """ Save sampled points menu command to save the last points sampled showed in the graphical view. """ fname = tkFileDialog.asksaveasfilename( initialdir=".", title="Select data file to save", filetypes=(("dps files", "*.dps"), ("all files", "*.*"))) if fname: self.scope.save(fname) def mnucmdedtmem(self): """ Memory menu command to edit the values of preset memories on DSP. """ if self.isconnected(): Meminterface(self.root, self.dps, self.updatefields) def mnucmdabout(self): """ About menu command to show the window with program information. """ Txtinterface( self.root, 'About', """DPS interface is designed by Simone Pernice That project was born as a textual driver to interface any DPS device. After the driver I made also a graphical interface to manage DPS devices. This project was born because I was not able to find any Linux application to manage DPS devices via USB. For question email to me: [email protected] Version {} relesed on {} First release on 3rd February 2019 Turin Italy DPS interface is under licence {} If you like this program please make a donation with PayPal to [email protected]""" .format(__version__, __date__, __license__)) def mnucmdhelp(self): """ Help menu command to show basic help on usage. """ Txtinterface( self.root, 'Help', """This is an interface to remote controll a power supplier of DPS series. The white fields can be edited, the gray are read only. To connect to DPS power supplier first link it to the PC through an USB cable. The data required to connect is on the first row of the graphical interface. Write the serial address on the first field (COMxx for Windows or /dev/ttyUSBx for Linux). Address and baudrate do not require update because they are the default for DPS power supplier. Turn on DPS with up key pressed to change those values. Press 'Connect' check button and if the device is present it is linked. Press again the same check button to disconnect the DPS. Once the link to DPS is in place all the data on the interface are updated and on the DPS the keylock is set. The second block of graphical interface contains all data about DPS. The brightness set which can be changed through the scale regulation. The model number. The memory from which recall the preset parameters. The input voltage, the output mode cv (constant voltage) or cc (constant current). The protection mode: none (no protection triggered), ovp (over voltage protection), ocp (over current protection), opp (over power protection). The maximum voltage, current and power to provide before triggering the self protection. The next row contains output voltage, current and power in textual form. A time diagram of the DPS output voltage, current and power is avaiable. It is possible to play with the mouse on that screen: - wheel press to fit in the screen all the enabled drawings - wheel to zoom in time - shift+wheel to zoom on Y for the highlighted curves - ctrl+wheel to change the enabled curves - left button drag to move the highlighted curve The same mouse functions are available in the fields below the diagram: - voltage per division, current per division and watt per division - zero position for voltage, current and power - check button to view voltage, current and power - time: second per divisions and zero position for time The sample time is used for the acquisition. DPS is quite slot, the minimum read time is around 1 second. The suggested rate is to have a sample for displayed pixel. The next buttons are: - Run acquisition: starts a thread that read the DPS status, update the interface fields as well as the time diagram. The acquisition points can be saved and loaded to be showed lated with menu commands on DPS scope load/save. They can be also edited through the wave edit window and played. - Key lock: set or reset the DPS key lock. It should be on in order to have faster communication. If key lock is on less fields of DPS are read since user can change them only through the PC interface. - Output enable to enable the DPS output Eventually there are the voltage and current scale. Thery are split in two: - the first is for coarse (1 unit/step) adjustment the unit of voltage/current - the second is for fine (0.01 unit/step) adjustament of voltage/current On the last block of interface there is a waveform field showing the wave loaded. Wave is a set of required output voltage and current at give timings. It is possible play and pause it through the respective commands of the interface. If loop is set when the wave play is completed it restarts. The acquisition may slow the wave player, use low acquisition sample time to avoid delays.""") def butcmdconnect(self): """ Connect check button command to connect to the DSP. It reads: serial port address, DPS address and serial speed from other interface fields. If it is capable to link to the DPS: - the maximum voltage and current are read and scale maximums set accordingly - the DPS current data are read and set accordingly in the interface - the localDPS interface is locked so that the user cannot change them but has to go through the graphical interface if the DPS is locked the polling is faster because less data needs to be read from DPS - the input fields are disabled """ if self.ivarconctd.get(): try: flds = self.svardpsaddbrt.get().split(',') if len(flds) > 0: ch = int(flds[0]) else: ch = 1 if len(flds) > 1: br = int(flds[1]) else: br = 9600 self.dps = DPSdriver(self.svardpsport.get(), ch, br) except Exception as e: tkMessageBox.showerror('Error', 'Cannot connect: ' + str(e)) self.ivarconctd.set(0) self.dps = None return m = self.dps.get(['model']) m = m[0] self.ivarmodel.set(m) to = m / 100 self.voltscale.config(to=to) self.maxoutv = to to = m % 100 self.curntscale.config(to=to) self.maxoutv = to self.scope.resetpoints() self.ivarkeylock.set(1) self.butcmdkeylock() self.updatefields(True) self.entryserport.config(state='readonly') self.entrydpsadd.config(state='readonly') else: # Stop polling self.ivaracquire.set(0) if self.poller: self.poller.wake() time.sleep(1.) # Wait to be sure the thread exits # Stop waveform generation self.ivarplaywv.set(0) if self.waver: self.waver.wake() time.sleep(1.) # Wait to be sure the thread exits self.dps = None self.entryserport.config(state=NORMAL) self.entrydpsadd.config(state=NORMAL) def butcmdacquire(self): """ Acquire check button command to manage the acquisition thread to read the DSP data. If the button is not selected the thread is lunched. If the button is selected the thread is stopped. """ if self.ivaracquire.get(): if not self.isconnected(): self.ivaracquire.set(0) return self.scope.resetpoints() self.strtme = time() self.poller = Poller(self.ivaracquire, self.dvarsecsmp, self.updatefields) else: self.poller.wake() def butcmdkeylock(self): """ Key lock button command to enable or disable the key lock on DPS remote interface. """ if self.isconnected(): self.dps.set(['lock'], [self.ivarkeylock.get()]) else: self.ivarkeylock.set(0) def butcmdoutenable(self): """ DPS output button command to enable or disable the DPS output power. """ if self.isconnected(): self.dps.set(['onoff'], [self.ivaroutenab.get()]) else: self.ivaroutenab.set(0) def butcmdplaywave(self): """ Wave generator check button command to manage the wave generation thread to make a waveform on the DSP. If the button is not selected the thread is lunched. If the button is selected the thread is stopped. """ if self.ivarplaywv.get(): if not self.isconnected(): self.ivarplaywv.set(0) return if not self.dpsfwave: tkMessageBox.showinfo('No wave in memory', 'Load or create a wave file to modify') self.ivarplaywv.set(0) return if not self.ivaroutenab.get(): self.ivaroutenab.set(1) self.butcmdoutenable() self.waver = Waver(self.setvcdps, self.ivarplaywv, self.ivarpausewv, self.ivarloopwv, self.dpsfwave.getpoints()) else: self.waver.wake() def butcmdpausewave(self): """ Wave generator pause check button command to temporary pause the wave generations. """ self.waver.wake() def wnwcmdclose(self): """ DPS main window close. Before exiting the supplier is disconnected the external supplier. """ if self.ivarconctd.get(): self.ivarconctd.set(0) self.butcmdconnect() self.root.destroy() def entbndvmax(self, event): """ Maximum voltage entry bind to set the protection maximum ouput voltage of the DSP. :param event: the event describing what changed """ if self.isconnected(): if self.dvarvmaxm0.get() > self.maxoutv * PROTEXCEED: self.dvarvmaxm0.set(self.maxoutv * PROTEXCEED) elif self.dvarvmaxm0.get() < 0.: self.dvarvmaxm0.set(0.) self.dps.set(['m0ovp'], [self.dvarvmaxm0.get()]) def entbndcmax(self, event): """ Maximum current entry bind to set the protection maximum output curret of the DSP. :param event: the event describing what changed """ if self.isconnected(): if self.dvarcmaxm0.get() > self.maxoutc * PROTEXCEED: self.dvarcmaxm0.set(self.maxoutc * PROTEXCEED) elif self.dvarcmaxm0.get() < 0.: self.dvarcmaxm0.set(0.) self.dps.set(['m0ocp'], [self.dvarcmaxm0.get()]) def entbndpmax(self, event): """ Maximum power entry bind to set the protection maximum output power of the DSP. :param event: the event describing what changed """ if self.isconnected(): if self.dvarpmaxm0.get( ) > self.maxoutv * self.maxoutc * PROTEXCEED * PROTEXCEED: self.dvarpmaxm0.set(self.maxoutv * self.maxoutc * PROTEXCEED * PROTEXCEED) elif self.dvarpmaxm0.get() < 0.: self.dvarcmaxm0.set(0.) self.dps.set(['m0opp'], [self.dvarpmaxm0.get()]) def setvcdps(self, v, c): """ Set the DPS output voltage and current moving their scales accordingly. :param v: the required voltage, if negative it is not changed :param c: the required current, if negative it is not changed """ if v >= 0: if c >= 0: self.setvscale(v) self.setcscale(c) self.dps.set(['vset', 'cset'], [v, c]) else: self.setvscale(v) self.dps.set(['vset'], [v]) elif c >= 0: self.setcscale(c) self.dps.set(['cset'], [c]) def isconnected(self): """ Check if the DPS is connected, if not display a message. :returns: True if connected, False if not """ if self.dps is None: tkMessageBox.showinfo('Not connected', 'Enstablish a connection before') return False return True def setvscale(self, v): """ Set the voltage scale, nothing is changed on the DPS. :param v: the voltage to set """ if v > self.maxoutv: v = self.maxoutv elif v < 0: v = 0 self.dvarvscale.set(int(v)) self.dvarvscalef.set(round(v - int(v), 2)) def getvscale(self): """ Get the voltage scale set value. :returns: the voltage set """ return self.dvarvscale.get() + self.dvarvscalef.get() def setcscale(self, c): """ Set the current scale, nothing is changed on the DPS. :param c: the current to set """ if c > self.maxoutc: c = self.maxoutc elif c < 0: c = 0 self.dvarcscale.set(int(c)) self.dvarcscalef.set(round(c - int(c), 2)) def getcscale(self): """ Get the current scale set value. :returns: the current set """ return self.dvarcscale.get() + self.dvarcscalef.get() def updatefields(self, forcereadall=False): """ Reads data stored in DPS and updates the interface fields accordingly. In order to be as fast as possible, if keylock is enabled, reads only the fields that can change without uses access. If keylock is disabled all the fields are read because user may have changed something from the interface. :param forcereadall: if True read and update all the DPS fields regardless of the keylock status :returns: the point read. A point is made by (time, voltage, current, power) """ if not forcereadall and self.ivarkeylock.get( ): # If user keep locked fewer data are read, otherwise all data = self.dps.get( ['vout', 'cout', 'pout', 'vinp', 'lock', 'prot', 'cvcc']) self.dvarvout.set(data[0]) self.dvarcout.set(data[1]) self.dvarpout.set(data[2]) self.dvarvinp.set(data[3]) self.ivarkeylock.set(data[4]) self.setprotection(data[5]) self.setworkmode(data[6]) vcp = data[0:3] else: # All data is read data = self.dps.get([ 'vset', 'cset', 'vout', 'cout', 'pout', 'vinp', 'lock', 'prot', 'cvcc', 'onoff', 'brght', 'mset', 'm0ovp', 'm0ocp', 'm0opp' ]) self.setvscale(data[0]) self.setcscale(data[1]) self.dvarvout.set(data[2]) self.dvarcout.set(data[3]) self.dvarpout.set(data[4]) self.dvarvinp.set(data[5]) self.ivarkeylock.set(data[6]) self.setprotection(data[7]) self.setworkmode(data[8]) self.ivaroutenab.set(data[9]) self.ivarbrghtnes.set(data[10]) self.ivarsetmem.set(data[11]) self.dvarvmaxm0.set(data[12]) self.dvarcmaxm0.set(data[13]) self.dvarpmaxm0.set(data[14]) vcp = data[2:5] vcp.insert(TPOS, time() - self.strtme) self.scope.addpoint(vcp) return vcp def setprotection(self, p): """ Set the protection field with an user readable string explaining the DPS protection status. :param p: the protection statu returned by the DPS """ self.svarprot.set({0: 'none', 1: 'ovp', 2: 'ocp', 3: 'opp'}[p]) def setworkmode(self, wm): """ Set the workmode field with an user readable string explaining the DPS work mode. :param wm: the working mode returned by the DPS """ self.svarwrmde.set({0: 'cv', 1: 'cc'}[wm])
class Cockpit(ttkFrame): ''' Remote device GUI ''' #TODO: 20160415 DPM - Set these values from configuration file #--- config THROTTLE_BY_USER = True THROTTLE_RESOLUTION = 0.1 # Joystick enabled or not, if any JOYSTICK_ENABLED = True DEFAULT_DRONE_IP = "192.168.1.130" DEFAULT_DRONE_PORT = 2121 #--- end config KEY_ANG_SPEED = "ang-speed" KEY_ANGLES = "angles" KEY_ACCEL = "accel" PID_KEYS = ["P", "I", "D"] DIR_NONE = 0 DIR_VERTICAL = 1 DIR_HORIZONTAL = 2 def __init__(self, parent, isDummy = False, droneIp = DEFAULT_DRONE_IP, dronePort = DEFAULT_DRONE_PORT): ''' Constructor ''' ttkFrame.__init__(self, parent) self._target = [0.0] * 4 self._selectedPidConstats = "--" self._pidConstants = { Cockpit.KEY_ANG_SPEED:{ "X":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Y":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Z":{ "P": 0.0, "I": 0.0, "D": 0.0 } }, Cockpit.KEY_ANGLES: { "X":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Y":{ "P": 0.0, "I": 0.0, "D": 0.0 } }, Cockpit.KEY_ACCEL:{ "X":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Y":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Z":{ "P": 0.0, "I": 0.0, "D": 0.0 } } } self.parent = parent self.initUI() self._controlKeysLocked = False if not isDummy: self._link = INetLink(droneIp, dronePort) else: self._link = ConsoleLink() self._link.open() self._updateInfoThread = Thread(target=self._updateInfo) self._updateInfoThreadRunning = False self._readingState = False self._start() def initUI(self): self.parent.title("Drone control") self.style = Style() self.style.theme_use("default") self.pack(fill=BOTH, expand=1) self.parent.bind_all("<Key>", self._keyDown) self.parent.bind_all("<KeyRelease>", self._keyUp) if system() == "Linux": self.parent.bind_all("<Button-4>", self._onMouseWheelUp) self.parent.bind_all("<Button-5>", self._onMouseWheelDown) else: #case of Windows self.parent.bind_all("<MouseWheel>", self._onMouseWheel) #Commands commandsFrame = tkFrame(self) commandsFrame.grid(column=0, row=0, sticky="WE") self._started = IntVar() self._startedCB = Checkbutton(commandsFrame, text="On", variable=self._started, command=self._startedCBChanged) self._startedCB.pack(side=LEFT, padx=4) # self._integralsCB = Checkbutton(commandsFrame, text="Int.", variable=self._integralsEnabled, \ # command=self._integralsCBChanged, state=DISABLED) # self._integralsCB.pack(side=LEFT, padx=4) self._quitButton = Button(commandsFrame, text="Quit", command=self.exit) self._quitButton.pack(side=LEFT, padx=2, pady=2) # self._angleLbl = Label(commandsFrame, text="Angle") # self._angleLbl.pack(side=LEFT, padx=4) # # self._angleEntry = Entry(commandsFrame, state=DISABLED) # self._angleEntry.pack(side=LEFT) #Info infoFrame = tkFrame(self) infoFrame.grid(column=1, row=1, sticky="NE", padx=4) #Throttle Label(infoFrame, text="Throttle").grid(column=0, row=0, sticky="WE") self._throttleTexts = [StringVar(),StringVar(),StringVar(),StringVar()] Entry(infoFrame, textvariable=self._throttleTexts[3], state=DISABLED, width=5).grid(column=0, row=1) Entry(infoFrame, textvariable=self._throttleTexts[0], state=DISABLED, width=5).grid(column=1, row=1) Entry(infoFrame, textvariable=self._throttleTexts[2], state=DISABLED, width=5).grid(column=0, row=2) Entry(infoFrame, textvariable=self._throttleTexts[1], state=DISABLED, width=5).grid(column=1, row=2) #Angles Label(infoFrame, text="Angles").grid(column=0, row=3, sticky="WE") self._angleTexts = [StringVar(),StringVar(),StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._angleTexts[index], state=DISABLED, width=5).grid(column=index, row=4) #Accels Label(infoFrame, text="Accels").grid(column=0, row=5, sticky="WE") self._accelTexts = [StringVar(),StringVar(),StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._accelTexts[index], state=DISABLED, width=5).grid(column=index, row=6) #Speeds Label(infoFrame, text="Speeds").grid(column=0, row=7, sticky="WE") self._speedTexts = [StringVar(),StringVar(),StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._speedTexts[index], state=DISABLED, width=5).grid(column=index, row=8) #Height Label(infoFrame, text="Height").grid(column=0, row=9, sticky="E") self._heightText = StringVar() Entry(infoFrame, textvariable=self._heightText, state=DISABLED, width=5).grid(column=1, row=9) #Loop rate Label(infoFrame, text="Loop @").grid(column=0, row=10, sticky="E") self._loopRateText = StringVar() Entry(infoFrame, textvariable=self._loopRateText, state=DISABLED, width=5).grid(column=1, row=10) Label(infoFrame, text="Hz").grid(column=2, row=10, sticky="W") #control controlFrame = tkFrame(self) controlFrame.grid(column=0, row=1, sticky="W") self._throttle = DoubleVar() if Cockpit.THROTTLE_BY_USER: self._thrustScale = Scale(controlFrame, orient=VERTICAL, from_=100.0, to=0.0, \ tickinterval=0, variable=self._throttle, resolution=Cockpit.THROTTLE_RESOLUTION, \ length=200, showvalue=1, \ state=DISABLED, command=self._onThrustScaleChanged) else: self._thrustScale = Scale(controlFrame, orient=VERTICAL, from_=100.0, to=-100.0, \ tickinterval=0, variable=self._throttle, \ length=200, showvalue=1, \ state=DISABLED, command=self._onThrustScaleChanged) self._thrustScale.bind("<Double-Button-1>", self._onThrustScaleDoubleButton1, "+") self._thrustScale.grid(column=0) self._shiftCanvas = Canvas(controlFrame, bg="white", height=400, width=400, \ relief=SUNKEN) self._shiftCanvas.bind("<Button-1>", self._onMouseButton1) #self._shiftCanvas.bind("<ButtonRelease-1>", self._onMouseButtonRelease1) self._shiftCanvas.bind("<B1-Motion>", self._onMouseButton1Motion) self._shiftCanvas.bind("<Double-Button-1>", self._onMouseDoubleButton1) self._shiftCanvas.bind("<Button-3>", self._onMouseButton3) #self._shiftCanvas.bind("<ButtonRelease-3>", self._onMouseButtonRelease3) self._shiftCanvas.bind("<B3-Motion>", self._onMouseButton3Motion) self._shiftCanvas.grid(row=0,column=1, padx=2, pady=2) self._shiftCanvas.create_oval(1, 1, 400, 400, outline="#ff0000") self._shiftCanvas.create_line(200, 2, 200, 400, fill="#ff0000") self._shiftCanvas.create_line(2, 200, 400, 200, fill="#ff0000") self._shiftMarker = self._shiftCanvas.create_oval(196, 196, 204, 204, outline="#0000ff", fill="#0000ff") self._yaw = DoubleVar() self._yawScale = Scale(controlFrame, orient=HORIZONTAL, from_=-100.0, to=100.0, \ tickinterval=0, variable=self._yaw, \ length=200, showvalue=1, \ command=self._onYawScaleChanged) self._yawScale.bind("<Double-Button-1>", self._onYawScaleDoubleButton1, "+") self._yawScale.grid(row=1, column=1) self._controlKeyActive = False #PID calibration pidCalibrationFrame = tkFrame(self) pidCalibrationFrame.grid(column=0, row=2, sticky="WE"); self._pidSelected = StringVar() self._pidSelected.set("--") self._pidListBox = OptionMenu(pidCalibrationFrame, self._pidSelected, "--", \ Cockpit.KEY_ANG_SPEED, Cockpit.KEY_ANGLES, Cockpit.KEY_ACCEL, \ command=self._onPidListBoxChanged) self._pidListBox.pack(side=LEFT, padx=2) self._pidListBox.config(width=10) self._axisSelected = StringVar() self._axisSelected.set("--") self._axisListBox = OptionMenu(pidCalibrationFrame, self._axisSelected, "--", "X", "Y", "Z", \ command=self._onAxisListBoxChanged) self._axisListBox.pack(side=LEFT, padx=2) self._axisListBox.config(state=DISABLED) Label(pidCalibrationFrame, text="P").pack(side=LEFT, padx=(14, 2)) self._pidPString = StringVar() self._pidPString.set("0.00") self._pidPSpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=10000.0, increment=0.01, state=DISABLED, \ textvariable=self._pidPString, command=self._onPidSpinboxChanged) self._pidPSpinbox.pack(side=LEFT, padx=2) Label(pidCalibrationFrame, text="I").pack(side=LEFT, padx=(14, 2)) self._pidIString = StringVar() self._pidIString.set("0.00") self._pidISpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=10000.0, increment=0.01, state=DISABLED, \ textvariable=self._pidIString, command=self._onPidSpinboxChanged) self._pidISpinbox.pack(side=LEFT, padx=2) Label(pidCalibrationFrame, text="D").pack(side=LEFT, padx=(14, 2)) self._pidDString = StringVar() self._pidDString.set("0.00") self._pidDSpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=10000.0, increment=0.01, state=DISABLED, \ textvariable=self._pidDString, command=self._onPidSpinboxChanged) self._pidDSpinbox.pack(side=LEFT, padx=2) #debug debugFrame = tkFrame(self) debugFrame.grid(column=0, row=3, sticky="WE") self._debugMsg = Message(debugFrame, anchor="nw", justify=LEFT, relief=SUNKEN, width=300) self._debugMsg.pack(fill=BOTH, expand=1) def _start(self): self._readDroneConfig() if Cockpit.JOYSTICK_ENABLED: self._joystickManager = JoystickManager.getInstance() self._joystickManager.start() joysticks = self._joystickManager.getJoysticks() if len(joysticks) != 0: self._joystick = joysticks[0] self._joystick.onAxisChanged += self._onJoystickAxisChanged self._joystick.onButtonPressed += self._onJoystickButtonPressed else: self._joystick = None def _onJoystickAxisChanged(self, sender, index): if self._started.get() and sender == self._joystick: axisValue = self._joystick.getAxisValue(index) if index == 0: self._yaw.set(axisValue) self._updateTarget() elif index == 1 and not Cockpit.THROTTLE_BY_USER: thrust = -axisValue self._throttle.set(thrust) self._updateTarget() elif index == 2 and Cockpit.THROTTLE_BY_USER: rowThrottle = (axisValue + 100.0)/2.0 if rowThrottle < 10.0: throttle = rowThrottle * 6.0 elif rowThrottle < 90.0: throttle = 60.0 + ((rowThrottle - 10.0) / 8.0) else: throttle = 70.0 + (rowThrottle - 90.0) * 3.0 self._throttle.set(throttle) self._sendThrottle() elif index == 3: x = 196 + axisValue * 2 lastCoords = self._shiftCanvas.coords(self._shiftMarker) coords = (x, lastCoords[1]) self._plotShiftCanvasMarker(coords) elif index == 4: y = 196 + axisValue * 2 lastCoords = self._shiftCanvas.coords(self._shiftMarker) coords = (lastCoords[0], y) self._plotShiftCanvasMarker(coords) def _onJoystickButtonPressed(self, sender, index): if sender == self._joystick and index == 7: if self._started.get() == 0: self._startedCB.select() else: self._startedCB.deselect() # Tkinter's widgets seem not to be calling the event-handler # when they are changed programmatically. Therefore, the # even-handler is called explicitly here. self._startedCBChanged() def exit(self): self._link.send({"key": "close", "data": None}) self._stopUpdateInfoThread() self._link.close() if Cockpit.JOYSTICK_ENABLED: self._joystickManager.stop() self.quit() def _updateTarget(self): markerCoords = self._shiftCanvas.coords(self._shiftMarker) coords = ((markerCoords[0] + markerCoords[2]) / 2, (markerCoords[1] + markerCoords[3]) / 2) self._target[0] = float(coords[1] - 200) / 2.0 # X-axis angle / X-axis acceleration self._target[1] = float(coords[0] - 200) / 2.0 # Y-axis angle / Y-axis acceleration #Remote control uses clockwise angle, but the drone's referece system uses counter-clockwise angle self._target[2] = -self._yaw.get() # Z-axis angular speed # Z-axis acceleration (thrust). Only when the motor throttle is not controlled by user directly if Cockpit.THROTTLE_BY_USER: self._target[3] = 0.0 else: self._target[3] = self._throttle.get() self._sendTarget() def _keyDown(self, event): if event.keysym == "Escape": self._throttle.set(0) self._started.set(0) self._thrustScale.config(state=DISABLED) self._stopUpdateInfoThread() self._sendIsStarted() elif event.keysym.startswith("Control"): self._controlKeyActive = True elif not self._controlKeysLocked and self._controlKeyActive: if event.keysym == "Up": self._thrustScaleUp() elif event.keysym == "Down": self._thrustScaleDown() elif event.keysym == "Left": self._yawLeft() elif event.keysym == "Right": self._yawRight() elif event.keysym == "space": self._yawReset() if not Cockpit.THROTTLE_BY_USER: self._thrustReset() elif not self._controlKeysLocked and not self._controlKeyActive: if event.keysym == "Up": self._moveShiftCanvasMarker((0,-5)) elif event.keysym == "Down": self._moveShiftCanvasMarker((0,5)) elif event.keysym == "Left": self._moveShiftCanvasMarker((-5,0)) elif event.keysym == "Right": self._moveShiftCanvasMarker((5,0)) elif event.keysym == "space": self._resetShiftCanvasMarker() def _keyUp(self, eventArgs): if eventArgs.keysym.startswith("Control"): self._controlKeyActive = False def _onMouseButton1(self, eventArgs): self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _onMouseButtonRelease1(self, eventArgs): self._shiftCanvas.coords(self._shiftMarker, 196, 196, 204, 204) def _limitCoordsToSize(self, coords, size, width): maxSize = size-(width/2.0) minSize = -(width/2.0) if coords[0] > maxSize: x = maxSize elif coords[0] < minSize: x = minSize else: x = coords[0] if coords[1] > maxSize: y = maxSize elif coords[1] < minSize: y = minSize else: y = coords[1] return (x,y) def _plotShiftCanvasMarker(self, coords): coords = self._limitCoordsToSize(coords, 400, 8) self._shiftCanvas.coords(self._shiftMarker, coords[0], coords[1], coords[0] + 8, coords[1] + 8) self._updateTarget() def _moveShiftCanvasMarker(self, shift): lastCoords = self._shiftCanvas.coords(self._shiftMarker) newCoords = (lastCoords[0] + shift[0], lastCoords[1] + shift[1]) self._plotShiftCanvasMarker(newCoords) def _resetShiftCanvasMarker(self): self._shiftCanvas.coords(self._shiftMarker, 196, 196, 204, 204) self._updateTarget() def _onMouseButton1Motion(self, eventArgs): deltaCoords = (eventArgs.x - self._lastMouseCoords[0], eventArgs.y - self._lastMouseCoords[1]) self._moveShiftCanvasMarker(deltaCoords) self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _onMouseDoubleButton1(self, eventArgs): self._resetShiftCanvasMarker() def _onMouseButton3(self, eventArgs): self._lastMouseCoords = (eventArgs.x, eventArgs.y) self._mouseDirection = Cockpit.DIR_NONE def _onMouseButtonRelease3(self, eventArgs): self._shiftCanvas.coords(self._shiftMarker, 196, 196, 204, 204) def _onMouseButton3Motion(self, eventArgs): deltaCoords = (eventArgs.x - self._lastMouseCoords[0], eventArgs.y - self._lastMouseCoords[1]) if self._mouseDirection == Cockpit.DIR_NONE: if abs(deltaCoords[0]) > abs(deltaCoords[1]): self._mouseDirection = Cockpit.DIR_HORIZONTAL else: self._mouseDirection = Cockpit.DIR_VERTICAL if self._mouseDirection == Cockpit.DIR_HORIZONTAL: deltaCoords = (deltaCoords[0], 0) else: deltaCoords = (0, deltaCoords[1]) self._moveShiftCanvasMarker(deltaCoords) self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _thrustScaleUp(self): #TODO: 20160526 DPM: El valor de incremento de aceleración (1.0) puede ser muy alto if self._started.get(): newValue = self._thrustScale.get() \ + (Cockpit.THROTTLE_RESOLUTION if Cockpit.THROTTLE_BY_USER else 1.0) self._thrustScale.set(newValue) self._updateTarget() def _thrustScaleDown(self): #TODO: 20160526 DPM: El valor de decremento de aceleración (1.0) puede ser muy alto if self._started.get(): newValue = self._thrustScale.get() \ - (Cockpit.THROTTLE_RESOLUTION if Cockpit.THROTTLE_BY_USER else 1.0) self._thrustScale.set(newValue) self._updateTarget() def _thrustReset(self): if self._started.get(): self._thrustScale.set(0.0) self._updateTarget() def _onThrustScaleDoubleButton1(self, eventArgs): self._thrustReset() return "break" def _yawRight(self): newValue = self._yaw.get() + 1 self._yaw.set(newValue) self._updateTarget() def _yawLeft(self): newValue = self._yaw.get() - 1 self._yaw.set(newValue) self._updateTarget() def _yawReset(self): self._yaw.set(0) self._updateTarget() def _onMouseWheelUp(self, eventArgs): if not self._controlKeyActive: self._thrustScaleUp() else: self._yawRight() def _onMouseWheelDown(self, eventArgs): if not self._controlKeyActive: self._thrustScaleDown() else: self._yawLeft() def _onMouseWheel(self, eventArgs): factor = eventArgs.delta/(1200.0 if Cockpit.THROTTLE_BY_USER and not self._controlKeyActive else 120.0) if not self._controlKeyActive: if self._started.get(): newValue = self._thrustScale.get() + factor self._thrustScale.set(newValue) self._updateTarget() else: newValue = self._yaw.get() + factor self._yaw.set(newValue) self._updateTarget() def _onYawScaleChanged(self, eventArgs): self._updateTarget() def _onYawScaleDoubleButton1(self, eventArgs): self._yawReset() return "break" def _startedCBChanged(self): if not self._started.get(): self._throttle.set(0) self._thrustScale.config(state=DISABLED) #self._integralsCB.config(state=DISABLED) self._stopUpdateInfoThread() else: self._thrustScale.config(state="normal") #self._integralsCB.config(state="normal") self._startUpdateInfoThread() self._sendIsStarted() # def _integralsCBChanged(self): # # self._link.send({"key": "integrals", "data":self._integralsEnabled.get() != 0}) # def _onThrustScaleChanged(self, eventArgs): if Cockpit.THROTTLE_BY_USER: self._sendThrottle() else: self._updateTarget() def _sendThrottle(self): self._link.send({"key": "throttle", "data": self._throttle.get()}) def _sendTarget(self): self._link.send({"key": "target", "data": self._target}) def _sendIsStarted(self): isStarted = self._started.get() != 0 self._link.send({"key": "is-started", "data": isStarted}) def _sendPidCalibrationData(self): if self._pidSelected.get() != "--" and self._axisSelected.get() != "--": pidData = { "pid": self._pidSelected.get(), "axis": self._axisSelected.get(), "p": float(self._pidPSpinbox.get()), "i": float(self._pidISpinbox.get()), "d": float(self._pidDSpinbox.get())} self._link.send({"key": "pid-calibration", "data": pidData}) def _updatePidCalibrationData(self): pid = self._pidSelected.get() axis = self._axisSelected.get() if pid != "--" and axis != "--": self._pidConstants[pid][axis]["P"] = float(self._pidPSpinbox.get()) self._pidConstants[pid][axis]["I"] = float(self._pidISpinbox.get()) self._pidConstants[pid][axis]["D"] = float(self._pidDSpinbox.get()) def _readDroneConfig(self): self._link.send({"key": "read-drone-config", "data": None}, self._onDroneConfigRead) def _readDroneState(self): if not self._readingState: self._readingState = True self._link.send({"key": "read-drone-state", "data": None}, self._onDroneStateRead) def _readPidConfigItem(self, message, cockpitKey, axises, configKeys): for i in range(len(axises)): for j in range(len(Cockpit.PID_KEYS)): self._pidConstants[cockpitKey][axises[i]][Cockpit.PID_KEYS[j]] = message[configKeys[j]][i] def _onDroneConfigRead(self, message): #TODO Show current configuration within the GUI (at least relevant settings) if message: #Angle-speeds self._readPidConfigItem(message, Cockpit.KEY_ANG_SPEED, ["X", "Y", "Z"], \ [Configuration.PID_ANGLES_SPEED_KP, \ Configuration.PID_ANGLES_SPEED_KI, \ Configuration.PID_ANGLES_SPEED_KD]) #Angles self._readPidConfigItem(message, Cockpit.KEY_ANGLES, ["X", "Y"], \ [Configuration.PID_ANGLES_KP, \ Configuration.PID_ANGLES_KI, \ Configuration.PID_ANGLES_KD]) #Accels self._readPidConfigItem(message, Cockpit.KEY_ACCEL, ["X", "Y", "Z"], \ [Configuration.PID_ACCEL_KP, \ Configuration.PID_ACCEL_KI, \ Configuration.PID_ACCEL_KD]) def _onDroneStateRead(self, state): if state: for index in range(4): self._throttleTexts[index].set("{0:.3f}".format(state["_throttles"][index])) for index in range(3): self._accelTexts[index].set("{0:.3f}".format(state["_accels"][index])) self._angleTexts[index].set("{0:.3f}".format(state["_angles"][index])) currentPeriod = state["_currentPeriod"] if currentPeriod > 0.0: freq = 1.0/currentPeriod self._loopRateText.set("{0:.3f}".format(freq)) else: self._loopRateText.set("--") else: self._stopUpdateInfoThread() self._readingState = False def _onPidSpinboxChanged(self): self._updatePidCalibrationData() self._sendPidCalibrationData() def _onPidListBoxChanged(self, pid): self._axisSelected.set("--") self._pidPString.set("--") self._pidIString.set("--") self._pidDString.set("--") self._pidPSpinbox.config(state=DISABLED) self._pidISpinbox.config(state=DISABLED) self._pidDSpinbox.config(state=DISABLED) self._selectedPidConstats = pid if pid == "--": self._axisListBox.config(state=DISABLED) self._controlKeysLocked = False else: self._axisListBox.config(state="normal") self._controlKeysLocked = True def _onAxisListBoxChanged(self, axis): if axis == "--" or (self._selectedPidConstats == Cockpit.KEY_ANGLES and axis == "Z"): self._pidPString.set("--") self._pidIString.set("--") self._pidDString.set("--") self._pidPSpinbox.config(state=DISABLED) self._pidISpinbox.config(state=DISABLED) self._pidDSpinbox.config(state=DISABLED) self._controlKeysLocked = axis != "--" else: self._pidPString.set("{:.2f}".format(self._pidConstants[self._selectedPidConstats][axis]["P"])) self._pidIString.set("{:.2f}".format(self._pidConstants[self._selectedPidConstats][axis]["I"])) self._pidDString.set("{:.2f}".format(self._pidConstants[self._selectedPidConstats][axis]["D"])) self._pidPSpinbox.config(state="normal") self._pidISpinbox.config(state="normal") self._pidDSpinbox.config(state="normal") self._controlKeysLocked = True def _updateInfo(self): while self._updateInfoThreadRunning: self._readDroneState() time.sleep(1.0) def _startUpdateInfoThread(self): self._updateInfoThreadRunning = True if not self._updateInfoThread.isAlive(): self._updateInfoThread.start() def _stopUpdateInfoThread(self): self._updateInfoThreadRunning = False if self._updateInfoThread.isAlive(): self._updateInfoThread.join()
class EktaproGUI(Tk): """ Constructs the main program window and interfaces with the EktaproController and the TimerController to access the slide projectors. """ def __init__(self): self.controller = EktaproController() self.controller.initDevices() Tk.__init__(self) self.protocol("WM_DELETE_WINDOW", self.onQuit) self.wm_title("EktaproGUI") self.bind("<Prior>", self.priorPressed) self.bind("<Next>", self.nextPressed) self.brightness = 0 self.slide = 1 self.timerController = TimerController(self.controller, self) self.controlPanel = Frame(self) self.manualPanel = Frame(self) self.projektorList = Listbox(self, selectmode=SINGLE) for i in range(len(self.controller.devices)): self.projektorList.insert(END, "[" + str(i) + "] " + str(self.controller.devices[i])) if self.projektorList.size >= 1: self.projektorList.selection_set(0) self.projektorList.bind("<ButtonRelease>", self.projektorSelectionChanged) self.projektorList.config(width=50) self.initButton = Button(self.controlPanel, text="init", command=self.initButtonPressed) self.nextButton = Button(self.controlPanel, text="next slide", command=self.nextSlidePressed) self.nextButton.config(state=DISABLED) self.prevButton = Button(self.controlPanel, text="previous slide", command=self.prevSlidePressed) self.prevButton.config(state=DISABLED) self.startButton = Button(self.controlPanel, text="start timer", command=self.startTimer) self.startButton.config(state=DISABLED) self.pauseButton = Button(self.controlPanel, text="pause", command=self.pauseTimer) self.stopButton = Button(self.controlPanel, text="stop", command=self.stopTimer) self.stopButton.config(state=DISABLED) self.timerLabel = Label(self.controlPanel, text="delay:") self.timerInput = Entry(self.controlPanel, width=3) self.timerInput.insert(0, "5") self.timerInput.config(state=DISABLED) self.timerInput.bind("<KeyPress-Return>", self.inputValuesChanged) self.timerInput.bind("<ButtonRelease>", self.updateGUI) self.fadeLabel = Label(self.controlPanel, text="fade:") self.fadeInput = Entry(self.controlPanel, width=3) self.fadeInput.insert(0, "1") self.fadeInput.config(state=DISABLED) self.fadeInput.bind("<KeyPress-Return>", self.inputValuesChanged) self.fadeInput.bind("<ButtonRelease>", self.updateGUI) self.standbyButton = Button(self.controlPanel, text="standby", command=self.toggleStandby) self.standbyButton.config(state=DISABLED) self.syncButton = Button(self.controlPanel, text="sync", command=self.sync) self.syncButton.config(state=DISABLED) self.reconnectButton = Button(self.controlPanel, text="reconnect", command=self.reconnect) self.cycle = IntVar() self.cycleButton = Checkbutton( self.controlPanel, text="use all projectors", variable=self.cycle, command=self.cycleToggled ) self.brightnessScale = Scale(self.manualPanel, from_=0, to=100, resolution=1, label="brightness") self.brightnessScale.set(self.brightness) self.brightnessScale.bind("<ButtonRelease>", self.brightnessChanged) self.brightnessScale.config(state=DISABLED) self.brightnessScale.config(orient=HORIZONTAL) self.brightnessScale.config(length=400) self.gotoSlideScale = Scale(self.manualPanel, from_=0, to=self.controller.maxTray, label="goto slide") self.gotoSlideScale.set(1) self.gotoSlideScale.bind("<ButtonRelease>", self.gotoSlideChanged) self.gotoSlideScale.config(state=DISABLED) self.gotoSlideScale.config(orient=HORIZONTAL) self.gotoSlideScale.config(length=400) self.controlPanel.pack(side=BOTTOM, anchor=W, fill=X) self.projektorList.pack(side=LEFT, fill=BOTH) self.manualPanel.pack(side=RIGHT, expand=1, fill=BOTH) self.initButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.prevButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.nextButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.cycleButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.startButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.pauseButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.stopButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.timerLabel.pack(side=LEFT, anchor=N, padx=4, pady=4) self.timerInput.pack(side=LEFT, anchor=N, padx=4, pady=4) self.fadeLabel.pack(side=LEFT, anchor=N, padx=4, pady=4) self.fadeInput.pack(side=LEFT, anchor=N, padx=4, pady=4) self.syncButton.pack(side=RIGHT, anchor=N, padx=4, pady=4) self.standbyButton.pack(side=RIGHT, anchor=N, padx=4, pady=4) self.reconnectButton.pack(side=RIGHT, anchor=N, padx=4, pady=4) self.brightnessScale.pack(side=TOP, anchor=W, expand=1, fill=X) self.gotoSlideScale.pack(side=TOP, anchor=W, expand=1, fill=X) self.menubar = Menu(self) self.toolsmenu = Menu(self.menubar) self.helpmenu = Menu(self.menubar) self.filemenu = Menu(self.menubar) self.toolsmenu.add_command(label="Interpret HEX Sequence", command=self.interpretHEXDialog) self.helpmenu.add_command( label="About EktaproGUI", command=lambda: tkMessageBox.showinfo("About EktaproGUI", "EktaproGUI 1.0 (C)opyright Julian Hoch 2010"), ) self.filemenu.add_command(label="Exit", command=self.onQuit) self.menubar.add_cascade(label="File", menu=self.filemenu) self.menubar.add_cascade(label="Tools", menu=self.toolsmenu) self.menubar.add_cascade(label="Help", menu=self.helpmenu) self.configure(menu=self.menubar) def initButtonPressed(self): self.controller.resetDevices() self.updateGUI() self.brightnessScale.config(state=NORMAL) self.gotoSlideScale.config(state=NORMAL) self.nextButton.config(state=NORMAL) self.prevButton.config(state=NORMAL) self.startButton.config(state=NORMAL) self.timerInput.config(state=NORMAL) self.fadeInput.config(state=NORMAL) self.syncButton.config(state=NORMAL) self.standbyButton.config(state=NORMAL) def inputValuesChanged(self, event): try: fadeDelay = int(self.fadeInput.get()) slideshowDelay = int(self.timerInput.get()) if fadeDelay in range(0, 60): self.timerController.fadeDelay = fadeDelay if slideshowDelay in range(1, 60): self.timerController.slideshowDelay = slideshowDelay except Exception: pass self.updateGUI() def sync(self): self.controller.syncDevices() self.updateGUI() def reconnect(self): self.controller.cleanUp() self.controller.initDevices() self.updateGUI() self.projektorList.delete(0, END) for i in range(len(self.controller.devices)): self.projektorList.insert(END, "[" + str(i) + "] " + str(self.controller.devices[i])) if self.projektorList.size >= 1: self.projektorList.selection_set(0) def projektorSelectionChanged(self, event): items = map(int, self.projektorList.curselection()) if self.controller.setActiveDevice(items): self.updateGUI() def updateGUI(self, event=None): if self.controller.activeDevice == None: return self.brightness = self.controller.activeDevice.brightness self.brightnessScale.set(self.brightness) self.slide = self.controller.activeDevice.slide self.gotoSlideScale.set(self.slide) for i in range(self.projektorList.size()): if i == self.controller.activeIndex: self.projektorList.selection_set(i) else: self.projektorList.selection_clear(i) def brightnessChanged(self, event): newBrightness = self.brightnessScale.get() if not self.brightness == newBrightness and not self.controller.activeDevice == None: self.controller.activeDevice.setBrightness(newBrightness) self.brightness = self.brightnessScale.get() def gotoSlideChanged(self, event): if self.controller.activeDevice is None: return newSlide = self.gotoSlideScale.get() if not self.slide == newSlide: self.controller.activeDevice.gotoSlide(newSlide) self.slide = newSlide def nextSlidePressed(self): if self.controller.activeDevice is None: return self.timerController.fadePaused = False self.timerController.nextSlide() self.updateGUI() def prevSlidePressed(self): if self.controller.activeDevice is None: return self.timerController.fadePaused = False self.timerController.previousSlide() self.updateGUI() def startTimer(self): self.stopButton.config(state=NORMAL) self.startButton.config(state=DISABLED) self.timerController.startSlideshow() def pauseTimer(self): if self.timerController.fadePaused or self.timerController.slideshowPaused: self.pauseButton.config(text="pause") self.timerController.resume() self.updateGUI() else: self.pauseButton.config(text="resume") self.timerController.pause() self.updateGUI() def stopTimer(self): self.pauseButton.config(text="pause") self.stopButton.config(state=DISABLED) self.startButton.config(state=NORMAL) self.timerController.stopSlideshow() self.updateGUI() def cycleToggled(self): self.timerController.cycle = True if self.cycle.get() == 1 else False def interpretHEXDialog(self): interpretDialog = InterpretHEXDialog(self) # @UnusedVariable def toggleStandby(self): if self.pauseButton.config()["text"][4] == "pause" and self.pauseButton.config()["state"][4] == "normal": self.pauseTimer() self.controller.toggleStandby() def nextPressed(self, event): if self.startButton.config()["state"][4] == "disabled": self.pauseTimer() else: self.nextSlidePressed() def priorPressed(self, event): if self.startButton.config()["state"][4] == "disabled": self.toggleStandby() else: self.prevSlidePressed() def onQuit(self): self.controller.cleanUp() self.destroy()
class Cockpit(ttkFrame): ''' Remote controller GUI ''' KEY_ANG_SPEED = "ang-speed" KEY_ANGLES = "angles" KEY_ACCEL = "accel" PID_KEYS = ["P", "I", "D"] DEFAULT_DRONE_IP = "192.168.1.130" DEFAULT_DRONE_PORT = 2121 DIR_NONE = 0 DIR_VERTICAL = 1 DIR_HORIZONTAL = 2 MAX_ACCEL = 10.0 #TODO angles. Replace by m/s² MAX_ACCEL_Z = 0.1 #m/s² MAX_ANGLE_SPEED = 50.0 #º/s def __init__(self, parent, isDummy = False, droneIp = DEFAULT_DRONE_IP, dronePort = DEFAULT_DRONE_PORT): ''' Constructor ''' ttkFrame.__init__(self, parent) self._started = IntVar() self._integralsEnabled = IntVar() self._target = [0.0] * 4 self._selectedPidConstats = "--" self._pidConstants = { Cockpit.KEY_ANG_SPEED:{ "X":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Y":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Z":{ "P": 0.0, "I": 0.0, "D": 0.0 } }, Cockpit.KEY_ANGLES: { "X":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Y":{ "P": 0.0, "I": 0.0, "D": 0.0 } }, Cockpit.KEY_ACCEL:{ "X":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Y":{ "P": 0.0, "I": 0.0, "D": 0.0 }, "Z":{ "P": 0.0, "I": 0.0, "D": 0.0 } } } self.parent = parent self.initUI() self._controlKeysLocked = False if not isDummy: self._link = INetLink(droneIp, dronePort) else: self._link = ConsoleLink() self._link.open() self._updateInfoThread = Thread(target=self._updateInfo) self._updateInfoThreadRunning = False self._readingState = False self._start() def initUI(self): self.parent.title("Drone control") self.style = Style() self.style.theme_use("default") self.pack(fill=BOTH, expand=1) self.parent.bind_all("<Key>", self._keyDown) self.parent.bind_all("<KeyRelease>", self._keyUp) if system() == "Linux": self.parent.bind_all("<Button-4>", self._onMouseWheelUp) self.parent.bind_all("<Button-5>", self._onMouseWheelDown) else: #case of Windows self.parent.bind_all("<MouseWheel>", self._onMouseWheel) #Commands commandsFrame = tkFrame(self) commandsFrame.grid(column=0, row=0, sticky="WE") self._startedCB = Checkbutton(commandsFrame, text="On", variable=self._started, command=self._startedCBChanged) self._startedCB.pack(side=LEFT, padx=4) self._integralsCB = Checkbutton(commandsFrame, text="Int.", variable=self._integralsEnabled, \ command=self._integralsCBChanged, state=DISABLED) self._integralsCB.pack(side=LEFT, padx=4) self._quitButton = Button(commandsFrame, text="Quit", command=self.exit) self._quitButton.pack(side=LEFT, padx=2, pady=2) # self._angleLbl = Label(commandsFrame, text="Angle") # self._angleLbl.pack(side=LEFT, padx=4) # # self._angleEntry = Entry(commandsFrame, state=DISABLED) # self._angleEntry.pack(side=LEFT) #Info infoFrame = tkFrame(self) infoFrame.grid(column=1, row=1, sticky="E", padx=4) #Throttle Label(infoFrame, text="Throttle").grid(column=0, row=0, sticky="WE") self._throttleTexts = [StringVar(),StringVar(),StringVar(),StringVar()] Entry(infoFrame, textvariable=self._throttleTexts[3], state=DISABLED, width=5).grid(column=0, row=1) Entry(infoFrame, textvariable=self._throttleTexts[0], state=DISABLED, width=5).grid(column=1, row=1) Entry(infoFrame, textvariable=self._throttleTexts[2], state=DISABLED, width=5).grid(column=0, row=2) Entry(infoFrame, textvariable=self._throttleTexts[1], state=DISABLED, width=5).grid(column=1, row=2) #Angles Label(infoFrame, text="Angles").grid(column=0, row=3, sticky="WE") self._angleTexts = [StringVar(),StringVar(),StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._angleTexts[index], state=DISABLED, width=5).grid(column=index, row=4) #Accels Label(infoFrame, text="Accels").grid(column=0, row=5, sticky="WE") self._accelTexts = [StringVar(),StringVar(),StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._accelTexts[index], state=DISABLED, width=5).grid(column=index, row=6) #Speeds Label(infoFrame, text="Speeds").grid(column=0, row=7, sticky="WE") self._speedTexts = [StringVar(),StringVar(),StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._speedTexts[index], state=DISABLED, width=5).grid(column=index, row=8) #Height Label(infoFrame, text="Height").grid(column=0, row=9, sticky="W") self._heightText = StringVar() Entry(infoFrame, state=DISABLED, width=5).grid(column=1, row=9) #control controlFrame = tkFrame(self) controlFrame.grid(column=0, row=1, sticky="W") self._throttle = DoubleVar() self._thrustScale = Scale(controlFrame, orient=VERTICAL, from_=100.0, to=-100.0, \ tickinterval=0, variable=self._throttle, \ length=200, showvalue=1, \ state=DISABLED, command=self._onThrustScaleChanged) self._thrustScale.bind("<Double-Button-1>", self._onThrustScaleDoubleButton1, "+") self._thrustScale.grid(column=0) self._shiftCanvas = Canvas(controlFrame, bg="white", height=400, width=400, \ relief=SUNKEN) self._shiftCanvas.bind("<Button-1>", self._onMouseButton1) #self._shiftCanvas.bind("<ButtonRelease-1>", self._onMouseButtonRelease1) self._shiftCanvas.bind("<B1-Motion>", self._onMouseButton1Motion) self._shiftCanvas.bind("<Double-Button-1>", self._onMouseDoubleButton1) self._shiftCanvas.bind("<Button-3>", self._onMouseButton3) #self._shiftCanvas.bind("<ButtonRelease-3>", self._onMouseButtonRelease3) self._shiftCanvas.bind("<B3-Motion>", self._onMouseButton3Motion) self._shiftCanvas.grid(row=0,column=1, padx=2, pady=2) self._shiftCanvas.create_oval(2, 2, 400, 400, outline="#ff0000") self._shiftCanvas.create_line(201, 2, 201, 400, fill="#ff0000") self._shiftCanvas.create_line(2, 201, 400, 201, fill="#ff0000") self._shiftMarker = self._shiftCanvas.create_oval(197, 197, 205, 205, outline="#0000ff", fill="#0000ff") self._yaw = DoubleVar() self._yawScale = Scale(controlFrame, orient=HORIZONTAL, from_=-100.0, to=100.0, \ tickinterval=0, variable=self._yaw, \ length=200, showvalue=1, \ command=self._onYawScaleChanged) self._yawScale.bind("<Double-Button-1>", self._onYawScaleDoubleButton1, "+") self._yawScale.grid(row=1, column=1) self._controlKeyActive = False #PID calibration pidCalibrationFrame = tkFrame(self) pidCalibrationFrame.grid(column=0, row=2, sticky="WE"); self._pidSelected = StringVar() self._pidSelected.set("--") self._pidListBox = OptionMenu(pidCalibrationFrame, self._pidSelected, "--", \ Cockpit.KEY_ANG_SPEED, Cockpit.KEY_ANGLES, Cockpit.KEY_ACCEL, \ command=self._onPidListBoxChanged) self._pidListBox.pack(side=LEFT, padx=2) self._pidListBox.config(width=10) self._axisSelected = StringVar() self._axisSelected.set("--") self._axisListBox = OptionMenu(pidCalibrationFrame, self._axisSelected, "--", "X", "Y", "Z", \ command=self._onAxisListBoxChanged) self._axisListBox.pack(side=LEFT, padx=2) self._axisListBox.config(state=DISABLED) Label(pidCalibrationFrame, text="P").pack(side=LEFT, padx=(14, 2)) self._pidPString = StringVar() self._pidPString.set("0.00") self._pidPSpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=100.0, increment=0.01, state=DISABLED, \ textvariable=self._pidPString, command=self._onPidSpinboxChanged) self._pidPSpinbox.pack(side=LEFT, padx=2) Label(pidCalibrationFrame, text="I").pack(side=LEFT, padx=(14, 2)) self._pidIString = StringVar() self._pidIString.set("0.00") self._pidISpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=100.0, increment=0.01, state=DISABLED, \ textvariable=self._pidIString, command=self._onPidSpinboxChanged) self._pidISpinbox.pack(side=LEFT, padx=2) Label(pidCalibrationFrame, text="D").pack(side=LEFT, padx=(14, 2)) self._pidDString = StringVar() self._pidDString.set("0.00") self._pidDSpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=100.0, increment=0.01, state=DISABLED, \ textvariable=self._pidDString, command=self._onPidSpinboxChanged) self._pidDSpinbox.pack(side=LEFT, padx=2) #debug debugFrame = tkFrame(self) debugFrame.grid(column=0, row=3, sticky="WE") self._debugMsg = Message(debugFrame, anchor="nw", justify=LEFT, relief=SUNKEN, width=300) self._debugMsg.pack(fill=BOTH, expand=1) def _start(self): self._readDroneConfig() def exit(self): self._link.send({"key": "close", "data": None}) self._stopUpdateInfoThread() self._link.close() self.quit() def _updateTarget(self): markerCoords = self._shiftCanvas.coords(self._shiftMarker) coords = ((markerCoords[0] + markerCoords[2]) / 2, (markerCoords[1] + markerCoords[3]) / 2) self._target[1] = float(coords[0] - 201) * Cockpit.MAX_ACCEL / 200.0 self._target[0] = float(coords[1] - 201) * Cockpit.MAX_ACCEL / 200.0 #Remote control uses clockwise angle, but the drone's referece system uses counter-clockwise angle self._target[2] = -self._yaw.get() * Cockpit.MAX_ANGLE_SPEED / 100.0 self._target[3] = self._throttle.get() * Cockpit.MAX_ACCEL_Z / 100.0 self._sendTarget() def _keyDown(self, event): if event.keysym == "Escape": self._throttle.set(0) self._started.set(0) self._thrustScale.config(state=DISABLED) self._stopUpdateInfoThread() self._sendIsStarted() elif event.keysym.startswith("Control"): self._controlKeyActive = True elif not self._controlKeysLocked and self._controlKeyActive: if event.keysym == "Up": self._thrustScaleUp() elif event.keysym == "Down": self._thrustScaleDown() elif event.keysym == "Left": self._yawLeft() elif event.keysym == "Right": self._yawRight() elif event.keysym == "space": self._yawReset() self._thrustReset() elif not self._controlKeysLocked and not self._controlKeyActive: if event.keysym == "Up": self._moveShiftCanvasMarker((0,-5)) elif event.keysym == "Down": self._moveShiftCanvasMarker((0,5)) elif event.keysym == "Left": self._moveShiftCanvasMarker((-5,0)) elif event.keysym == "Right": self._moveShiftCanvasMarker((5,0)) elif event.keysym == "space": self._resetShiftCanvasMarker() def _keyUp(self, eventArgs): if eventArgs.keysym.startswith("Control"): self._controlKeyActive = False def _onMouseButton1(self, eventArgs): self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _onMouseButtonRelease1(self, eventArgs): self._shiftCanvas.coords(self._shiftMarker, 197, 197, 205, 205) def _limitCoordsToSize(self, coords, size): if coords[0] > size: x = size elif coords[0] < 0: x = 0 else: x = coords[0] if coords[1] > size: y = size elif coords[1] < 0: y = 0 else: y = coords[1] return (x,y) def _moveShiftCanvasMarker(self, shift): lastCoords = self._shiftCanvas.coords(self._shiftMarker) newCoords = (lastCoords[0] + shift[0], lastCoords[1] + shift[1]) newCoords = self._limitCoordsToSize(newCoords, 400) self._shiftCanvas.coords(self._shiftMarker, newCoords[0], newCoords[1], newCoords[0] + 8, newCoords[1] + 8) self._updateTarget() def _resetShiftCanvasMarker(self): self._shiftCanvas.coords(self._shiftMarker, 197, 197, 205, 205) self._updateTarget() def _onMouseButton1Motion(self, eventArgs): deltaCoords = (eventArgs.x - self._lastMouseCoords[0], eventArgs.y - self._lastMouseCoords[1]) self._moveShiftCanvasMarker(deltaCoords) self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _onMouseDoubleButton1(self, eventArgs): self._resetShiftCanvasMarker() def _onMouseButton3(self, eventArgs): self._lastMouseCoords = (eventArgs.x, eventArgs.y) self._mouseDirection = Cockpit.DIR_NONE def _onMouseButtonRelease3(self, eventArgs): self._shiftCanvas.coords(self._shiftMarker, 197, 197, 205, 205) def _onMouseButton3Motion(self, eventArgs): deltaCoords = (eventArgs.x - self._lastMouseCoords[0], eventArgs.y - self._lastMouseCoords[1]) if self._mouseDirection == Cockpit.DIR_NONE: if abs(deltaCoords[0]) > abs(deltaCoords[1]): self._mouseDirection = Cockpit.DIR_HORIZONTAL else: self._mouseDirection = Cockpit.DIR_VERTICAL if self._mouseDirection == Cockpit.DIR_HORIZONTAL: deltaCoords = (deltaCoords[0], 0) else: deltaCoords = (0, deltaCoords[1]) self._moveShiftCanvasMarker(deltaCoords) self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _thrustScaleUp(self): if self._started.get(): newValue = self._thrustScale.get() + 1 self._thrustScale.set(newValue) self._updateTarget() def _thrustScaleDown(self): if self._started.get(): newValue = self._thrustScale.get() - 1 self._thrustScale.set(newValue) self._updateTarget() def _thrustReset(self): if self._started.get(): self._thrustScale.set(0.0) self._updateTarget() def _onThrustScaleDoubleButton1(self, eventArgs): self._thrustReset() return "break" def _yawRight(self): newValue = self._yaw.get() + 1 self._yaw.set(newValue) self._updateTarget() def _yawLeft(self): newValue = self._yaw.get() - 1 self._yaw.set(newValue) self._updateTarget() def _yawReset(self): self._yaw.set(0) self._updateTarget() def _onMouseWheelUp(self, eventArgs): if not self._controlKeyActive: self._thrustScaleUp() else: self._yawRight() def _onMouseWheelDown(self, eventArgs): if not self._controlKeyActive: self._thrustScaleDown() else: self._yawLeft() def _onMouseWheel(self, eventArgs): factor = int(eventArgs.delta/120) if not self._controlKeyActive: if self._started.get(): newValue = self._thrustScale.get() + factor self._thrustScale.set(newValue) self._updateTarget() else: newValue = self._yaw.get() + factor self._yaw.set(newValue) self._updateTarget() def _onYawScaleChanged(self, eventArgs): self._updateTarget() def _onYawScaleDoubleButton1(self, eventArgs): self._yawReset() return "break" def _startedCBChanged(self): if not self._started.get(): self._throttle.set(0) self._thrustScale.config(state=DISABLED) self._integralsCB.config(state=DISABLED) self._stopUpdateInfoThread() else: self._thrustScale.config(state="normal") self._integralsCB.config(state="normal") self._startUpdateInfoThread() self._sendIsStarted() def _integralsCBChanged(self): self._link.send({"key": "integrals", "data":self._integralsEnabled.get() != 0}) def _onThrustScaleChanged(self, eventArgs): self._updateTarget() def _sendTarget(self): self._link.send({"key": "target", "data": self._target}) def _sendIsStarted(self): isStarted = self._started.get() != 0 self._link.send({"key": "is-started", "data": isStarted}) def _sendPidCalibrationData(self): if self._pidSelected.get() != "--" and self._axisSelected.get() != "--": pidData = { "pid": self._pidSelected.get(), "axis": self._axisSelected.get(), "p": float(self._pidPSpinbox.get()), "i": float(self._pidISpinbox.get()), "d": float(self._pidDSpinbox.get())} self._link.send({"key": "pid-calibration", "data": pidData}) def _updatePidCalibrationData(self): pid = self._pidSelected.get() axis = self._axisSelected.get() if pid != "--" and axis != "--": self._pidConstants[pid][axis]["P"] = float(self._pidPSpinbox.get()) self._pidConstants[pid][axis]["I"] = float(self._pidISpinbox.get()) self._pidConstants[pid][axis]["D"] = float(self._pidDSpinbox.get()) def _readDroneConfig(self): self._link.send({"key": "read-drone-config", "data": None}, self._onDroneConfigRead) def _readDroneState(self): if not self._readingState: self._readingState = True self._link.send({"key": "read-drone-state", "data": None}, self._onDroneStateRead) def _readPidConfigItem(self, message, cockpitKey, axises, configKeys): for i in range(len(axises)): for j in range(len(Cockpit.PID_KEYS)): self._pidConstants[cockpitKey][axises[i]][Cockpit.PID_KEYS[j]] = message[configKeys[j]][i] def _onDroneConfigRead(self, message): #TODO Show current configuration within the GUI (at least relevant settings) if message: #Angle-speeds self._readPidConfigItem(message, Cockpit.KEY_ANG_SPEED, ["X", "Y", "Z"], \ [Configuration.PID_ANGLES_SPEED_KP, \ Configuration.PID_ANGLES_SPEED_KI, \ Configuration.PID_ANGLES_SPEED_KD]) #Angles self._readPidConfigItem(message, Cockpit.KEY_ANGLES, ["X", "Y"], \ [Configuration.PID_ANGLES_KP, \ Configuration.PID_ANGLES_KI, \ Configuration.PID_ANGLES_KD]) #Accels self._readPidConfigItem(message, Cockpit.KEY_ACCEL, ["X", "Y", "Z"], \ [Configuration.PID_ACCEL_KP, \ Configuration.PID_ACCEL_KI, \ Configuration.PID_ACCEL_KD]) def _onDroneStateRead(self, state): if state: for index in range(4): self._throttleTexts[index].set("{0:.3f}".format(state["_throttles"][index])) for index in range(3): self._accelTexts[index].set("{0:.3f}".format(state["_accels"][index])) self._angleTexts[index].set("{0:.3f}".format(state["_angles"][index])) else: self._stopUpdateInfoThread() self._readingState = False def _onPidSpinboxChanged(self): self._updatePidCalibrationData() self._sendPidCalibrationData() def _onPidListBoxChanged(self, pid): self._axisSelected.set("--") self._pidPString.set("--") self._pidIString.set("--") self._pidDString.set("--") self._pidPSpinbox.config(state=DISABLED) self._pidISpinbox.config(state=DISABLED) self._pidDSpinbox.config(state=DISABLED) self._selectedPidConstats = pid if pid == "--": self._axisListBox.config(state=DISABLED) self._controlKeysLocked = False else: self._axisListBox.config(state="normal") self._controlKeysLocked = True def _onAxisListBoxChanged(self, axis): if axis == "--" or (self._selectedPidConstats == Cockpit.KEY_ANGLES and axis == "Z"): self._pidPString.set("--") self._pidIString.set("--") self._pidDString.set("--") self._pidPSpinbox.config(state=DISABLED) self._pidISpinbox.config(state=DISABLED) self._pidDSpinbox.config(state=DISABLED) self._controlKeysLocked = axis != "--" else: self._pidPString.set("{:.2f}".format(self._pidConstants[self._selectedPidConstats][axis]["P"])) self._pidIString.set("{:.2f}".format(self._pidConstants[self._selectedPidConstats][axis]["I"])) self._pidDString.set("{:.2f}".format(self._pidConstants[self._selectedPidConstats][axis]["D"])) self._pidPSpinbox.config(state="normal") self._pidISpinbox.config(state="normal") self._pidDSpinbox.config(state="normal") self._controlKeysLocked = True def _updateInfo(self): while self._updateInfoThreadRunning: self._readDroneState() time.sleep(1.0) def _startUpdateInfoThread(self): self._updateInfoThreadRunning = True if not self._updateInfoThread.isAlive(): self._updateInfoThread.start() def _stopUpdateInfoThread(self): self._updateInfoThreadRunning = False if self._updateInfoThread.isAlive(): self._updateInfoThread.join()
class Cockpit(ttkFrame): ''' Remote device GUI ''' #TODO: 20160415 DPM - Set these values from configuration file #--- config THROTTLE_BY_USER = True THROTTLE_RESOLUTION = 0.1 # Joystick enabled or not, if any JOYSTICK_ENABLED = True DEFAULT_DRONE_IP = "192.168.1.130" DEFAULT_DRONE_PORT = 2121 #--- end config KEY_ANG_SPEED = "ang-speed" KEY_ANGLES = "angles" KEY_ACCEL = "accel" PID_KEYS = ["P", "I", "D"] DIR_NONE = 0 DIR_VERTICAL = 1 DIR_HORIZONTAL = 2 def __init__(self, parent, isDummy=False, droneIp=DEFAULT_DRONE_IP, dronePort=DEFAULT_DRONE_PORT): ''' Constructor ''' ttkFrame.__init__(self, parent) self._target = [0.0] * 4 self._selectedPidConstats = "--" self._pidConstants = { Cockpit.KEY_ANG_SPEED: { "X": { "P": 0.0, "I": 0.0, "D": 0.0 }, "Y": { "P": 0.0, "I": 0.0, "D": 0.0 }, "Z": { "P": 0.0, "I": 0.0, "D": 0.0 } }, Cockpit.KEY_ANGLES: { "X": { "P": 0.0, "I": 0.0, "D": 0.0 }, "Y": { "P": 0.0, "I": 0.0, "D": 0.0 } }, Cockpit.KEY_ACCEL: { "X": { "P": 0.0, "I": 0.0, "D": 0.0 }, "Y": { "P": 0.0, "I": 0.0, "D": 0.0 }, "Z": { "P": 0.0, "I": 0.0, "D": 0.0 } } } self.parent = parent self.initUI() self._controlKeysLocked = False if not isDummy: self._link = INetLink(droneIp, dronePort) else: self._link = ConsoleLink() self._link.open() self._updateInfoThread = Thread(target=self._updateInfo) self._updateInfoThreadRunning = False self._readingState = False self._start() def initUI(self): self.parent.title("Drone control") self.style = Style() self.style.theme_use("default") self.pack(fill=BOTH, expand=1) self.parent.bind_all("<Key>", self._keyDown) self.parent.bind_all("<KeyRelease>", self._keyUp) if system() == "Linux": self.parent.bind_all("<Button-4>", self._onMouseWheelUp) self.parent.bind_all("<Button-5>", self._onMouseWheelDown) else: #case of Windows self.parent.bind_all("<MouseWheel>", self._onMouseWheel) #Commands commandsFrame = tkFrame(self) commandsFrame.grid(column=0, row=0, sticky="WE") self._started = IntVar() self._startedCB = Checkbutton(commandsFrame, text="On", variable=self._started, command=self._startedCBChanged) self._startedCB.pack(side=LEFT, padx=4) # self._integralsCB = Checkbutton(commandsFrame, text="Int.", variable=self._integralsEnabled, \ # command=self._integralsCBChanged, state=DISABLED) # self._integralsCB.pack(side=LEFT, padx=4) self._quitButton = Button(commandsFrame, text="Quit", command=self.exit) self._quitButton.pack(side=LEFT, padx=2, pady=2) # self._angleLbl = Label(commandsFrame, text="Angle") # self._angleLbl.pack(side=LEFT, padx=4) # # self._angleEntry = Entry(commandsFrame, state=DISABLED) # self._angleEntry.pack(side=LEFT) #Info infoFrame = tkFrame(self) infoFrame.grid(column=1, row=1, sticky="NE", padx=4) #Throttle Label(infoFrame, text="Throttle").grid(column=0, row=0, sticky="WE") self._throttleTexts = [ StringVar(), StringVar(), StringVar(), StringVar() ] Entry(infoFrame, textvariable=self._throttleTexts[3], state=DISABLED, width=5).grid(column=0, row=1) Entry(infoFrame, textvariable=self._throttleTexts[0], state=DISABLED, width=5).grid(column=1, row=1) Entry(infoFrame, textvariable=self._throttleTexts[2], state=DISABLED, width=5).grid(column=0, row=2) Entry(infoFrame, textvariable=self._throttleTexts[1], state=DISABLED, width=5).grid(column=1, row=2) #Angles Label(infoFrame, text="Angles").grid(column=0, row=3, sticky="WE") self._angleTexts = [StringVar(), StringVar(), StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._angleTexts[index], state=DISABLED, width=5).grid(column=index, row=4) #Accels Label(infoFrame, text="Accels").grid(column=0, row=5, sticky="WE") self._accelTexts = [StringVar(), StringVar(), StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._accelTexts[index], state=DISABLED, width=5).grid(column=index, row=6) #Speeds Label(infoFrame, text="Speeds").grid(column=0, row=7, sticky="WE") self._speedTexts = [StringVar(), StringVar(), StringVar()] for index in range(3): Entry(infoFrame, textvariable=self._speedTexts[index], state=DISABLED, width=5).grid(column=index, row=8) #Height Label(infoFrame, text="Height").grid(column=0, row=9, sticky="E") self._heightText = StringVar() Entry(infoFrame, textvariable=self._heightText, state=DISABLED, width=5).grid(column=1, row=9) #Loop rate Label(infoFrame, text="Loop @").grid(column=0, row=10, sticky="E") self._loopRateText = StringVar() Entry(infoFrame, textvariable=self._loopRateText, state=DISABLED, width=5).grid(column=1, row=10) Label(infoFrame, text="Hz").grid(column=2, row=10, sticky="W") #control controlFrame = tkFrame(self) controlFrame.grid(column=0, row=1, sticky="W") self._throttle = DoubleVar() if Cockpit.THROTTLE_BY_USER: self._thrustScale = Scale(controlFrame, orient=VERTICAL, from_=100.0, to=0.0, \ tickinterval=0, variable=self._throttle, resolution=Cockpit.THROTTLE_RESOLUTION, \ length=200, showvalue=1, \ state=DISABLED, command=self._onThrustScaleChanged) else: self._thrustScale = Scale(controlFrame, orient=VERTICAL, from_=100.0, to=-100.0, \ tickinterval=0, variable=self._throttle, \ length=200, showvalue=1, \ state=DISABLED, command=self._onThrustScaleChanged) self._thrustScale.bind("<Double-Button-1>", self._onThrustScaleDoubleButton1, "+") self._thrustScale.grid(column=0) self._shiftCanvas = Canvas(controlFrame, bg="white", height=400, width=400, \ relief=SUNKEN) self._shiftCanvas.bind("<Button-1>", self._onMouseButton1) #self._shiftCanvas.bind("<ButtonRelease-1>", self._onMouseButtonRelease1) self._shiftCanvas.bind("<B1-Motion>", self._onMouseButton1Motion) self._shiftCanvas.bind("<Double-Button-1>", self._onMouseDoubleButton1) self._shiftCanvas.bind("<Button-3>", self._onMouseButton3) #self._shiftCanvas.bind("<ButtonRelease-3>", self._onMouseButtonRelease3) self._shiftCanvas.bind("<B3-Motion>", self._onMouseButton3Motion) self._shiftCanvas.grid(row=0, column=1, padx=2, pady=2) self._shiftCanvas.create_oval(1, 1, 400, 400, outline="#ff0000") self._shiftCanvas.create_line(200, 2, 200, 400, fill="#ff0000") self._shiftCanvas.create_line(2, 200, 400, 200, fill="#ff0000") self._shiftMarker = self._shiftCanvas.create_oval(196, 196, 204, 204, outline="#0000ff", fill="#0000ff") self._yaw = DoubleVar() self._yawScale = Scale(controlFrame, orient=HORIZONTAL, from_=-100.0, to=100.0, \ tickinterval=0, variable=self._yaw, \ length=200, showvalue=1, \ command=self._onYawScaleChanged) self._yawScale.bind("<Double-Button-1>", self._onYawScaleDoubleButton1, "+") self._yawScale.grid(row=1, column=1) self._controlKeyActive = False #PID calibration pidCalibrationFrame = tkFrame(self) pidCalibrationFrame.grid(column=0, row=2, sticky="WE") self._pidSelected = StringVar() self._pidSelected.set("--") self._pidListBox = OptionMenu(pidCalibrationFrame, self._pidSelected, "--", \ Cockpit.KEY_ANG_SPEED, Cockpit.KEY_ANGLES, Cockpit.KEY_ACCEL, \ command=self._onPidListBoxChanged) self._pidListBox.pack(side=LEFT, padx=2) self._pidListBox.config(width=10) self._axisSelected = StringVar() self._axisSelected.set("--") self._axisListBox = OptionMenu(pidCalibrationFrame, self._axisSelected, "--", "X", "Y", "Z", \ command=self._onAxisListBoxChanged) self._axisListBox.pack(side=LEFT, padx=2) self._axisListBox.config(state=DISABLED) Label(pidCalibrationFrame, text="P").pack(side=LEFT, padx=(14, 2)) self._pidPString = StringVar() self._pidPString.set("0.00") self._pidPSpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=10000.0, increment=0.01, state=DISABLED, \ textvariable=self._pidPString, command=self._onPidSpinboxChanged) self._pidPSpinbox.pack(side=LEFT, padx=2) Label(pidCalibrationFrame, text="I").pack(side=LEFT, padx=(14, 2)) self._pidIString = StringVar() self._pidIString.set("0.00") self._pidISpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=10000.0, increment=0.01, state=DISABLED, \ textvariable=self._pidIString, command=self._onPidSpinboxChanged) self._pidISpinbox.pack(side=LEFT, padx=2) Label(pidCalibrationFrame, text="D").pack(side=LEFT, padx=(14, 2)) self._pidDString = StringVar() self._pidDString.set("0.00") self._pidDSpinbox = Spinbox(pidCalibrationFrame, width=5, from_=0.0, to=10000.0, increment=0.01, state=DISABLED, \ textvariable=self._pidDString, command=self._onPidSpinboxChanged) self._pidDSpinbox.pack(side=LEFT, padx=2) #debug debugFrame = tkFrame(self) debugFrame.grid(column=0, row=3, sticky="WE") self._debugMsg = Message(debugFrame, anchor="nw", justify=LEFT, relief=SUNKEN, width=300) self._debugMsg.pack(fill=BOTH, expand=1) def _start(self): self._readDroneConfig() if Cockpit.JOYSTICK_ENABLED: self._joystickManager = JoystickManager.getInstance() self._joystickManager.start() joysticks = self._joystickManager.getJoysticks() if len(joysticks) != 0: self._joystick = joysticks[0] self._joystick.onAxisChanged += self._onJoystickAxisChanged self._joystick.onButtonPressed += self._onJoystickButtonPressed else: self._joystick = None def _onJoystickAxisChanged(self, sender, index): if self._started.get() and sender == self._joystick: axisValue = self._joystick.getAxisValue(index) if index == 0: self._yaw.set(axisValue) self._updateTarget() elif index == 1 and not Cockpit.THROTTLE_BY_USER: thrust = -axisValue self._throttle.set(thrust) self._updateTarget() elif index == 2 and Cockpit.THROTTLE_BY_USER: rowThrottle = (axisValue + 100.0) / 2.0 if rowThrottle < 10.0: throttle = rowThrottle * 6.0 elif rowThrottle < 90.0: throttle = 60.0 + ((rowThrottle - 10.0) / 8.0) else: throttle = 70.0 + (rowThrottle - 90.0) * 3.0 self._throttle.set(throttle) self._sendThrottle() elif index == 3: x = 196 + axisValue * 2 lastCoords = self._shiftCanvas.coords(self._shiftMarker) coords = (x, lastCoords[1]) self._plotShiftCanvasMarker(coords) elif index == 4: y = 196 + axisValue * 2 lastCoords = self._shiftCanvas.coords(self._shiftMarker) coords = (lastCoords[0], y) self._plotShiftCanvasMarker(coords) def _onJoystickButtonPressed(self, sender, index): if sender == self._joystick and index == 7: if self._started.get() == 0: self._startedCB.select() else: self._startedCB.deselect() # Tkinter's widgets seem not to be calling the event-handler # when they are changed programmatically. Therefore, the # even-handler is called explicitly here. self._startedCBChanged() def exit(self): self._link.send({"key": "close", "data": None}) self._stopUpdateInfoThread() self._link.close() if Cockpit.JOYSTICK_ENABLED: self._joystickManager.stop() self.quit() def _updateTarget(self): markerCoords = self._shiftCanvas.coords(self._shiftMarker) coords = ((markerCoords[0] + markerCoords[2]) / 2, (markerCoords[1] + markerCoords[3]) / 2) self._target[0] = float( coords[1] - 200) / 2.0 # X-axis angle / X-axis acceleration self._target[1] = float( coords[0] - 200) / 2.0 # Y-axis angle / Y-axis acceleration #Remote control uses clockwise angle, but the drone's referece system uses counter-clockwise angle self._target[2] = -self._yaw.get() # Z-axis angular speed # Z-axis acceleration (thrust). Only when the motor throttle is not controlled by user directly if Cockpit.THROTTLE_BY_USER: self._target[3] = 0.0 else: self._target[3] = self._throttle.get() self._sendTarget() def _keyDown(self, event): if event.keysym == "Escape": self._throttle.set(0) self._started.set(0) self._thrustScale.config(state=DISABLED) self._stopUpdateInfoThread() self._sendIsStarted() elif event.keysym.startswith("Control"): self._controlKeyActive = True elif not self._controlKeysLocked and self._controlKeyActive: if event.keysym == "Up": self._thrustScaleUp() elif event.keysym == "Down": self._thrustScaleDown() elif event.keysym == "Left": self._yawLeft() elif event.keysym == "Right": self._yawRight() elif event.keysym == "space": self._yawReset() if not Cockpit.THROTTLE_BY_USER: self._thrustReset() elif not self._controlKeysLocked and not self._controlKeyActive: if event.keysym == "Up": self._moveShiftCanvasMarker((0, -5)) elif event.keysym == "Down": self._moveShiftCanvasMarker((0, 5)) elif event.keysym == "Left": self._moveShiftCanvasMarker((-5, 0)) elif event.keysym == "Right": self._moveShiftCanvasMarker((5, 0)) elif event.keysym == "space": self._resetShiftCanvasMarker() def _keyUp(self, eventArgs): if eventArgs.keysym.startswith("Control"): self._controlKeyActive = False def _onMouseButton1(self, eventArgs): self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _onMouseButtonRelease1(self, eventArgs): self._shiftCanvas.coords(self._shiftMarker, 196, 196, 204, 204) def _limitCoordsToSize(self, coords, size, width): maxSize = size - (width / 2.0) minSize = -(width / 2.0) if coords[0] > maxSize: x = maxSize elif coords[0] < minSize: x = minSize else: x = coords[0] if coords[1] > maxSize: y = maxSize elif coords[1] < minSize: y = minSize else: y = coords[1] return (x, y) def _plotShiftCanvasMarker(self, coords): coords = self._limitCoordsToSize(coords, 400, 8) self._shiftCanvas.coords(self._shiftMarker, coords[0], coords[1], coords[0] + 8, coords[1] + 8) self._updateTarget() def _moveShiftCanvasMarker(self, shift): lastCoords = self._shiftCanvas.coords(self._shiftMarker) newCoords = (lastCoords[0] + shift[0], lastCoords[1] + shift[1]) self._plotShiftCanvasMarker(newCoords) def _resetShiftCanvasMarker(self): self._shiftCanvas.coords(self._shiftMarker, 196, 196, 204, 204) self._updateTarget() def _onMouseButton1Motion(self, eventArgs): deltaCoords = (eventArgs.x - self._lastMouseCoords[0], eventArgs.y - self._lastMouseCoords[1]) self._moveShiftCanvasMarker(deltaCoords) self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _onMouseDoubleButton1(self, eventArgs): self._resetShiftCanvasMarker() def _onMouseButton3(self, eventArgs): self._lastMouseCoords = (eventArgs.x, eventArgs.y) self._mouseDirection = Cockpit.DIR_NONE def _onMouseButtonRelease3(self, eventArgs): self._shiftCanvas.coords(self._shiftMarker, 196, 196, 204, 204) def _onMouseButton3Motion(self, eventArgs): deltaCoords = (eventArgs.x - self._lastMouseCoords[0], eventArgs.y - self._lastMouseCoords[1]) if self._mouseDirection == Cockpit.DIR_NONE: if abs(deltaCoords[0]) > abs(deltaCoords[1]): self._mouseDirection = Cockpit.DIR_HORIZONTAL else: self._mouseDirection = Cockpit.DIR_VERTICAL if self._mouseDirection == Cockpit.DIR_HORIZONTAL: deltaCoords = (deltaCoords[0], 0) else: deltaCoords = (0, deltaCoords[1]) self._moveShiftCanvasMarker(deltaCoords) self._lastMouseCoords = (eventArgs.x, eventArgs.y) def _thrustScaleUp(self): #TODO: 20160526 DPM: El valor de incremento de aceleración (1.0) puede ser muy alto if self._started.get(): newValue = self._thrustScale.get() \ + (Cockpit.THROTTLE_RESOLUTION if Cockpit.THROTTLE_BY_USER else 1.0) self._thrustScale.set(newValue) self._updateTarget() def _thrustScaleDown(self): #TODO: 20160526 DPM: El valor de decremento de aceleración (1.0) puede ser muy alto if self._started.get(): newValue = self._thrustScale.get() \ - (Cockpit.THROTTLE_RESOLUTION if Cockpit.THROTTLE_BY_USER else 1.0) self._thrustScale.set(newValue) self._updateTarget() def _thrustReset(self): if self._started.get(): self._thrustScale.set(0.0) self._updateTarget() def _onThrustScaleDoubleButton1(self, eventArgs): self._thrustReset() return "break" def _yawRight(self): newValue = self._yaw.get() + 1 self._yaw.set(newValue) self._updateTarget() def _yawLeft(self): newValue = self._yaw.get() - 1 self._yaw.set(newValue) self._updateTarget() def _yawReset(self): self._yaw.set(0) self._updateTarget() def _onMouseWheelUp(self, eventArgs): if not self._controlKeyActive: self._thrustScaleUp() else: self._yawRight() def _onMouseWheelDown(self, eventArgs): if not self._controlKeyActive: self._thrustScaleDown() else: self._yawLeft() def _onMouseWheel(self, eventArgs): factor = eventArgs.delta / (1200.0 if Cockpit.THROTTLE_BY_USER and not self._controlKeyActive else 120.0) if not self._controlKeyActive: if self._started.get(): newValue = self._thrustScale.get() + factor self._thrustScale.set(newValue) self._updateTarget() else: newValue = self._yaw.get() + factor self._yaw.set(newValue) self._updateTarget() def _onYawScaleChanged(self, eventArgs): self._updateTarget() def _onYawScaleDoubleButton1(self, eventArgs): self._yawReset() return "break" def _startedCBChanged(self): if not self._started.get(): self._throttle.set(0) self._thrustScale.config(state=DISABLED) #self._integralsCB.config(state=DISABLED) self._stopUpdateInfoThread() else: self._thrustScale.config(state="normal") #self._integralsCB.config(state="normal") self._startUpdateInfoThread() self._sendIsStarted() # def _integralsCBChanged(self): # # self._link.send({"key": "integrals", "data":self._integralsEnabled.get() != 0}) # def _onThrustScaleChanged(self, eventArgs): if Cockpit.THROTTLE_BY_USER: self._sendThrottle() else: self._updateTarget() def _sendThrottle(self): self._link.send({"key": "throttle", "data": self._throttle.get()}) def _sendTarget(self): self._link.send({"key": "target", "data": self._target}) def _sendIsStarted(self): isStarted = self._started.get() != 0 self._link.send({"key": "is-started", "data": isStarted}) def _sendPidCalibrationData(self): if self._pidSelected.get() != "--" and self._axisSelected.get( ) != "--": pidData = { "pid": self._pidSelected.get(), "axis": self._axisSelected.get(), "p": float(self._pidPSpinbox.get()), "i": float(self._pidISpinbox.get()), "d": float(self._pidDSpinbox.get()) } self._link.send({"key": "pid-calibration", "data": pidData}) def _updatePidCalibrationData(self): pid = self._pidSelected.get() axis = self._axisSelected.get() if pid != "--" and axis != "--": self._pidConstants[pid][axis]["P"] = float(self._pidPSpinbox.get()) self._pidConstants[pid][axis]["I"] = float(self._pidISpinbox.get()) self._pidConstants[pid][axis]["D"] = float(self._pidDSpinbox.get()) def _readDroneConfig(self): self._link.send({ "key": "read-drone-config", "data": None }, self._onDroneConfigRead) def _readDroneState(self): if not self._readingState: self._readingState = True self._link.send({ "key": "read-drone-state", "data": None }, self._onDroneStateRead) def _readPidConfigItem(self, message, cockpitKey, axises, configKeys): for i in range(len(axises)): for j in range(len(Cockpit.PID_KEYS)): self._pidConstants[cockpitKey][axises[i]][ Cockpit.PID_KEYS[j]] = message[configKeys[j]][i] def _onDroneConfigRead(self, message): #TODO Show current configuration within the GUI (at least relevant settings) if message: #Angle-speeds self._readPidConfigItem(message, Cockpit.KEY_ANG_SPEED, ["X", "Y", "Z"], \ [Configuration.PID_ANGLES_SPEED_KP, \ Configuration.PID_ANGLES_SPEED_KI, \ Configuration.PID_ANGLES_SPEED_KD]) #Angles self._readPidConfigItem(message, Cockpit.KEY_ANGLES, ["X", "Y"], \ [Configuration.PID_ANGLES_KP, \ Configuration.PID_ANGLES_KI, \ Configuration.PID_ANGLES_KD]) #Accels self._readPidConfigItem(message, Cockpit.KEY_ACCEL, ["X", "Y", "Z"], \ [Configuration.PID_ACCEL_KP, \ Configuration.PID_ACCEL_KI, \ Configuration.PID_ACCEL_KD]) def _onDroneStateRead(self, state): if state: for index in range(4): self._throttleTexts[index].set("{0:.3f}".format( state["_throttles"][index])) for index in range(3): self._accelTexts[index].set("{0:.3f}".format( state["_accels"][index])) self._angleTexts[index].set("{0:.3f}".format( state["_angles"][index])) currentPeriod = state["_currentPeriod"] if currentPeriod > 0.0: freq = 1.0 / currentPeriod self._loopRateText.set("{0:.3f}".format(freq)) else: self._loopRateText.set("--") else: self._stopUpdateInfoThread() self._readingState = False def _onPidSpinboxChanged(self): self._updatePidCalibrationData() self._sendPidCalibrationData() def _onPidListBoxChanged(self, pid): self._axisSelected.set("--") self._pidPString.set("--") self._pidIString.set("--") self._pidDString.set("--") self._pidPSpinbox.config(state=DISABLED) self._pidISpinbox.config(state=DISABLED) self._pidDSpinbox.config(state=DISABLED) self._selectedPidConstats = pid if pid == "--": self._axisListBox.config(state=DISABLED) self._controlKeysLocked = False else: self._axisListBox.config(state="normal") self._controlKeysLocked = True def _onAxisListBoxChanged(self, axis): if axis == "--" or (self._selectedPidConstats == Cockpit.KEY_ANGLES and axis == "Z"): self._pidPString.set("--") self._pidIString.set("--") self._pidDString.set("--") self._pidPSpinbox.config(state=DISABLED) self._pidISpinbox.config(state=DISABLED) self._pidDSpinbox.config(state=DISABLED) self._controlKeysLocked = axis != "--" else: self._pidPString.set("{:.2f}".format( self._pidConstants[self._selectedPidConstats][axis]["P"])) self._pidIString.set("{:.2f}".format( self._pidConstants[self._selectedPidConstats][axis]["I"])) self._pidDString.set("{:.2f}".format( self._pidConstants[self._selectedPidConstats][axis]["D"])) self._pidPSpinbox.config(state="normal") self._pidISpinbox.config(state="normal") self._pidDSpinbox.config(state="normal") self._controlKeysLocked = True def _updateInfo(self): while self._updateInfoThreadRunning: self._readDroneState() time.sleep(1.0) def _startUpdateInfoThread(self): self._updateInfoThreadRunning = True if not self._updateInfoThread.isAlive(): self._updateInfoThread.start() def _stopUpdateInfoThread(self): self._updateInfoThreadRunning = False if self._updateInfoThread.isAlive(): self._updateInfoThread.join()
class EktaproGUI(Tk): """ Constructs the main program window and interfaces with the EktaproController and the TimerController to access the slide projectors. """ def __init__(self): self.controller = EktaproController() self.controller.initDevices() Tk.__init__(self) self.protocol('WM_DELETE_WINDOW', self.onQuit) self.wm_title("EktaproGUI") self.bind("<Prior>", self.priorPressed) self.bind("<Next>", self.nextPressed) self.brightness = 0 self.slide = 1 self.timerController = TimerController(self.controller, self) self.controlPanel = Frame(self) self.manualPanel = Frame(self) self.projektorList = Listbox(self, selectmode=SINGLE) for i in range(len(self.controller.devices)): self.projektorList.insert(END, \ "[" + str(i) + "] " + str(self.controller.devices[i])) if self.projektorList.size >= 1: self.projektorList.selection_set(0) self.projektorList.bind("<ButtonRelease>", \ self.projektorSelectionChanged) self.projektorList.config(width=50) self.initButton = Button(self.controlPanel, \ text="init", \ command=self.initButtonPressed) self.nextButton = Button(self.controlPanel, \ text="next slide", \ command=self.nextSlidePressed) self.nextButton.config(state=DISABLED) self.prevButton = Button(self.controlPanel, \ text="previous slide", \ command=self.prevSlidePressed) self.prevButton.config(state=DISABLED) self.startButton = Button(self.controlPanel, \ text="start timer", \ command=self.startTimer) self.startButton.config(state=DISABLED) self.pauseButton = Button(self.controlPanel, \ text="pause", \ command=self.pauseTimer) self.stopButton = Button(self.controlPanel, \ text="stop", \ command=self.stopTimer) self.stopButton.config(state=DISABLED) self.timerLabel = Label(self.controlPanel, \ text="delay:") self.timerInput = Entry(self.controlPanel, \ width=3) self.timerInput.insert(0, "5") self.timerInput.config(state=DISABLED) self.timerInput.bind("<KeyPress-Return>", self.inputValuesChanged) self.timerInput.bind("<ButtonRelease>", self.updateGUI) self.fadeLabel = Label(self.controlPanel, \ text="fade:") self.fadeInput = Entry(self.controlPanel, \ width=3) self.fadeInput.insert(0, "1") self.fadeInput.config(state=DISABLED) self.fadeInput.bind("<KeyPress-Return>", self.inputValuesChanged) self.fadeInput.bind("<ButtonRelease>", self.updateGUI) self.standbyButton = Button(self.controlPanel, \ text="standby", \ command=self.toggleStandby) self.standbyButton.config(state=DISABLED) self.syncButton = Button(self.controlPanel, \ text="sync", \ command=self.sync) self.syncButton.config(state=DISABLED) self.reconnectButton = Button(self.controlPanel, \ text="reconnect", \ command=self.reconnect) self.cycle = IntVar() self.cycleButton = Checkbutton(self.controlPanel, \ text="use all projectors", \ variable=self.cycle, \ command=self.cycleToggled) self.brightnessScale = Scale(self.manualPanel, from_=0, to=100, resolution=1, \ label="brightness") self.brightnessScale.set(self.brightness) self.brightnessScale.bind("<ButtonRelease>", self.brightnessChanged) self.brightnessScale.config(state=DISABLED) self.brightnessScale.config(orient=HORIZONTAL) self.brightnessScale.config(length=400) self.gotoSlideScale = Scale(self.manualPanel, \ from_=0, to=self.controller.maxTray, \ label="goto slide") self.gotoSlideScale.set(1) self.gotoSlideScale.bind("<ButtonRelease>", self.gotoSlideChanged) self.gotoSlideScale.config(state=DISABLED) self.gotoSlideScale.config(orient=HORIZONTAL) self.gotoSlideScale.config(length=400) self.controlPanel.pack(side=BOTTOM, anchor=W, fill=X) self.projektorList.pack(side=LEFT, fill=BOTH) self.manualPanel.pack(side=RIGHT, expand=1, fill=BOTH) self.initButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.prevButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.nextButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.cycleButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.startButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.pauseButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.stopButton.pack(side=LEFT, anchor=N, padx=4, pady=4) self.timerLabel.pack(side=LEFT, anchor=N, padx=4, pady=4) self.timerInput.pack(side=LEFT, anchor=N, padx=4, pady=4) self.fadeLabel.pack(side=LEFT, anchor=N, padx=4, pady=4) self.fadeInput.pack(side=LEFT, anchor=N, padx=4, pady=4) self.syncButton.pack(side=RIGHT, anchor=N, padx=4, pady=4) self.standbyButton.pack(side=RIGHT, anchor=N, padx=4, pady=4) self.reconnectButton.pack(side=RIGHT, anchor=N, padx=4, pady=4) self.brightnessScale.pack(side=TOP, anchor=W, expand=1, fill=X) self.gotoSlideScale.pack(side=TOP, anchor=W , expand=1, fill=X) self.menubar = Menu(self) self.toolsmenu = Menu(self.menubar) self.helpmenu = Menu(self.menubar) self.filemenu = Menu(self.menubar) self.toolsmenu.add_command(label="Interpret HEX Sequence", \ command=self.interpretHEXDialog) self.helpmenu.add_command(label="About EktaproGUI", \ command=lambda:tkMessageBox.showinfo("About EktaproGUI", \ "EktaproGUI 1.0 (C)opyright Julian Hoch 2010")) self.filemenu.add_command(label="Exit", command=self.onQuit) self.menubar.add_cascade(label="File", menu=self.filemenu) self.menubar.add_cascade(label="Tools", menu=self.toolsmenu) self.menubar.add_cascade(label="Help", menu=self.helpmenu) self.configure(menu=self.menubar) def initButtonPressed(self): self.controller.resetDevices() self.updateGUI() self.brightnessScale.config(state=NORMAL) self.gotoSlideScale.config(state=NORMAL) self.nextButton.config(state=NORMAL) self.prevButton.config(state=NORMAL) self.startButton.config(state=NORMAL) self.timerInput.config(state=NORMAL) self.fadeInput.config(state=NORMAL) self.syncButton.config(state=NORMAL) self.standbyButton.config(state=NORMAL) def inputValuesChanged(self, event): try: fadeDelay = int(self.fadeInput.get()) slideshowDelay = int(self.timerInput.get()) if fadeDelay in range(0, 60): self.timerController.fadeDelay = fadeDelay if slideshowDelay in range(1, 60): self.timerController.slideshowDelay = slideshowDelay except Exception: pass self.updateGUI() def sync(self): self.controller.syncDevices() self.updateGUI() def reconnect(self): self.controller.cleanUp() self.controller.initDevices() self.updateGUI() self.projektorList.delete(0, END) for i in range(len(self.controller.devices)): self.projektorList.insert(END, \ "[" + str(i) + "] " + str(self.controller.devices[i])) if self.projektorList.size >= 1: self.projektorList.selection_set(0) def projektorSelectionChanged(self, event): items = map(int, self.projektorList.curselection()) if self.controller.setActiveDevice(items): self.updateGUI() def updateGUI(self, event=None): if self.controller.activeDevice == None: return self.brightness = self.controller.activeDevice.brightness self.brightnessScale.set(self.brightness) self.slide = self.controller.activeDevice.slide self.gotoSlideScale.set(self.slide) for i in range(self.projektorList.size()): if i == self.controller.activeIndex: self.projektorList.selection_set(i) else: self.projektorList.selection_clear(i) def brightnessChanged(self, event): newBrightness = self.brightnessScale.get() if not self.brightness == newBrightness \ and not self.controller.activeDevice == None: self.controller.activeDevice.setBrightness(newBrightness) self.brightness = self.brightnessScale.get() def gotoSlideChanged(self, event): if self.controller.activeDevice is None: return newSlide = self.gotoSlideScale.get() if not self.slide == newSlide: self.controller.activeDevice.gotoSlide(newSlide) self.slide = newSlide def nextSlidePressed(self): if self.controller.activeDevice is None: return self.timerController.fadePaused = False self.timerController.nextSlide() self.updateGUI() def prevSlidePressed(self): if self.controller.activeDevice is None: return self.timerController.fadePaused = False self.timerController.previousSlide() self.updateGUI() def startTimer(self): self.stopButton.config(state=NORMAL) self.startButton.config(state=DISABLED) self.timerController.startSlideshow() def pauseTimer(self): if self.timerController.fadePaused or self.timerController.slideshowPaused: self.pauseButton.config(text="pause") self.timerController.resume() self.updateGUI() else: self.pauseButton.config(text="resume") self.timerController.pause() self.updateGUI() def stopTimer(self): self.pauseButton.config(text="pause") self.stopButton.config(state=DISABLED) self.startButton.config(state=NORMAL) self.timerController.stopSlideshow() self.updateGUI() def cycleToggled(self): self.timerController.cycle = True if self.cycle.get() == 1 else False def interpretHEXDialog(self): interpretDialog = InterpretHEXDialog(self) #@UnusedVariable def toggleStandby(self): if self.pauseButton.config()["text"][4] == "pause" \ and self.pauseButton.config()["state"][4] == "normal": self.pauseTimer() self.controller.toggleStandby() def nextPressed(self, event): if self.startButton.config()["state"][4] == "disabled": self.pauseTimer() else: self.nextSlidePressed() def priorPressed(self, event): if self.startButton.config()["state"][4] == "disabled": self.toggleStandby() else: self.prevSlidePressed() def onQuit(self): self.controller.cleanUp() self.destroy()