root/trunk/libffado/support/mixer-qt4/ffado/panelmanager.py

Revision 2670, 21.0 kB (checked in by jwoithe, 7 years ago)

Add the "-b" / "--bypassdbus" command line option to ffado-mixer. If given,
no communication with ffado-dbus-server will be attempted. Instead, an
instance of each supported mixer module will be created with stubs providing
placeholder values in place of values obtained from hardware. This permits
rudimentary debugging of mixer modules for interfaces which the developer
does not have on hand.

This commit amounts to a minor rework of a patch originally submitted by
Xavier Forestier as part of a large series which is being slowly merged:

https://sourceforge.net/p/ffado/mailman/message/35457569/.

The most significant difference is that the bypassdbus functionality in
Xavier's patch was a compile option whereas the commit implements it as a
runtime option. It is felt that this will make things easier for developers
in the long run: mixer modules can be tested without having to reconfigure
ffado from source. Minor bug fixes were applied to the patch as it was
reworked.

Thanks to Xavier Forestier for the original patch.

  • Property svn:mergeinfo set to
Line 
1 #
2 # Copyright (C) 2005-2008 by Pieter Palmers
3 #               2007-2008 by Arnold Krille
4 #               2013 by Philippe Carriere
5 #
6 # This file is part of FFADO
7 # FFADO = Free Firewire (pro-)audio drivers for linux
8 #
9 # FFADO is based upon FreeBoB.
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 #
24
25 from ffado.config import * #FFADO_VERSION, FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH
26
27 from PyQt4.QtGui import QFrame, QWidget, QTabWidget, QVBoxLayout, QMainWindow, QIcon, QAction, qApp, QStyleOptionTabWidgetFrame, QFileDialog
28 from PyQt4.QtCore import QTimer, pyqtSignal
29
30 from ffado.dbus_util import *
31 from ffado.registration import *
32
33 from ffado.configuration import *
34
35 from ffado.mixer.globalmixer import GlobalMixer
36 from ffado.mixer.dummy import Dummy
37
38 import time
39
40 import logging
41 log = logging.getLogger('panelmanager')
42
43 use_generic = False
44 try:
45     from mixer_generic import *
46     log.info("The generic mixer is found, seems to be a developer using ffadomixer...")
47 except ImportError:
48     pass
49 else:
50     use_generic = True
51
52 # pseudo-guid
53 GUID_GENERIC_MIXER = 0
54
55 FILE_VERSION = '0.1'
56
57 class HLine( QFrame ):
58     def __init__( self, parent ):
59         QFrame.__init__( self, parent )
60         self.setFrameShape( QFrame.HLine )
61         self.setLineWidth( 2 )
62         self.setMinimumHeight( 10 )
63
64 class PanelManagerStatus(QWidget):
65     def __init__(self, parent):
66         QWidget.__init__(self,parent)
67         uicLoad("ffado/panelmanagerstatus", self)
68
69 class OwnTabWidget(QTabWidget):
70     def __init__(self,parent):
71         QTabWidget.__init__(self,parent)
72
73     def tabInserted(self,index):
74         self.checkTabBar()
75
76     def tabRemoved(self,index):
77         self.checkTabBar()
78
79     def checkTabBar(self):
80         if self.count()<2:
81             self.tabBar().hide()
82         else:
83             self.tabBar().show()
84
85 class PanelManager(QWidget):
86     connectionLost = pyqtSignal(name='connectionLost')
87     def __init__(self, parent, devmgr=None):
88         QWidget.__init__(self, parent)
89         self.setObjectName("PanelManager")
90         self.parent = parent
91
92         # maps a device GUID to a QT panel
93         self.panels = {}
94
95         # a layout for ourselves
96         self.layout = QVBoxLayout(self)
97
98         # the tabs
99         self.tabs = OwnTabWidget(self)
100         self.tabs.hide()
101         self.layout.addWidget(self.tabs)
102
103         # a dialog that is shown during update
104         self.status = PanelManagerStatus(self)
105         self.layout.addWidget(self.status)
106         self.status.show()
107
108         self.devices = DeviceList( SYSTEM_CONFIG_FILE )
109         self.devices.updateFromFile( USER_CONFIG_FILE )
110
111         if devmgr is not None:
112             self.setManager(devmgr)
113
114     def __del__(self):
115         print("PanelManager.__del__()")
116         self.polltimer.stop()
117
118     def setManager(self,devmgr):
119         self.devmgr = devmgr
120         self.devmgr.registerPreUpdateCallback(self.devlistPreUpdate)
121         self.devmgr.registerPostUpdateCallback(self.devlistPostUpdate)
122         self.devmgr.registerUpdateCallback(self.devlistUpdate)
123         self.devmgr.registerDestroyedCallback(self.devmgrDestroyed)
124         # create a timer to poll the panels
125         self.polltimer = QTimer()
126         self.polltimer.timeout.connect(self.pollPanels)
127         self.polltimer.start( POLL_SLEEP_TIME_MSEC )
128
129         # create a timer to initialize the panel after the main form is shown
130         # since initialization can take a while
131         QTimer.singleShot( POLL_SLEEP_TIME_MSEC, self.updatePanels )
132
133         # live check timer
134         self.alivetimer = QTimer()
135         self.alivetimer.timeout.connect(self.commCheck)
136         self.alivetimer.start( 2000 )
137
138     def count(self):
139         return self.tabs.count()
140
141     def pollPanels(self):
142         #log.debug("PanelManager::pollPanels()")
143         # only when not modifying the tabs
144         try:
145             if self.tabs.isEnabled():
146                 for guid in self.panels.keys():
147                     w = self.panels[guid]
148                     for child in w.children():
149                         #log.debug("poll child %s,%s" % (guid,child))
150                         if 'polledUpdate' in dir(child):
151                             child.polledUpdate()
152         except:
153             log.error("error in pollPanels")
154             self.commCheck()
155
156     def devlistPreUpdate(self):
157         log.debug("devlistPreUpdate")
158         self.tabs.setEnabled(False)
159         self.tabs.hide()
160         self.status.lblMessage.setText("Bus reconfiguration in progress, please wait...")
161         self.status.show()
162         #self.statusBar().showMessage("bus reconfiguration in progress...", 5000)
163
164     def devlistPostUpdate(self):
165         log.debug("devlistPostUpdate")
166         # this can fail if multiple busresets happen in fast succession
167         ntries = 10
168         while ntries > 0:
169             try:
170                 self.updatePanels()
171                 return
172             except:
173                 log.debug("devlistPostUpdate failed (%d)" % ntries)
174                 for guid in self.panels.keys():
175                     w = self.panels[guid]
176                     del self.panels[guid] # remove from the list
177                     idx = self.tabs.indexOf(w)
178                     self.tabs.removeTab(idx)
179                     del w # GC might also take care of that
180
181                 ntries = ntries - 1
182                 time.sleep(2) # sleep a few seconds
183
184         log.debug("devlistPostUpdate failed completely")
185         self.tabs.setEnabled(False)
186         self.tabs.hide()
187         self.status.lblMessage.setText("Error while reconfiguring. Please restart ffadomixer.")
188         self.status.show()
189
190
191     def devlistUpdate(self):
192         log.debug("devlistUpdate")
193
194     def devmgrDestroyed(self):
195         log.debug("devmgrDestroyed")
196         self.alivetimer.stop()
197         self.tabs.setEnabled(False)
198         self.tabs.hide()
199         self.status.lblMessage.setText("DBUS server was shut down, please restart it and restart ffadomixer...")
200         self.status.show()
201
202     def commCheck(self):
203         try:
204             nbDevices = self.devmgr.getNbDevices()
205         except:
206             log.error("The communication with ffado-dbus-server was lost.")
207             self.tabs.setEnabled(False)
208             self.polltimer.stop()
209             self.alivetimer.stop()
210             keys = self.panels.keys()
211             for panel in keys:
212                 w = self.panels[panel]
213                 del self.panels[panel]
214                 w.deleteLater()
215             self.connectionLost.emit()
216
217     def removePanel(self, guid):
218         print "Removing widget for device" + guid
219         w = self.panels[guid]
220         del self.panels[guid] # remove from the list
221         idx = self.tabs.indexOf(w)
222         self.tabs.removeTab(idx)
223         w.deleteLater()
224
225     def addPanel(self, idx):
226         path = self.devmgr.getDeviceName(idx)
227         log.debug("Adding device %d: %s" % (idx, path))
228
229         if ffado.config.bypassdbus:
230             cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, path)
231         else:
232             cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
233         vendorId = cfgrom.getVendorId()
234         modelId = cfgrom.getModelId()
235         unitVersion = cfgrom.getUnitVersion()
236         guid = cfgrom.getGUID()
237         vendorName = cfgrom.getVendorName()
238         modelName = cfgrom.getModelName()
239         log.debug(" Found (%s, %X, %X) %s %s" % (str(guid), vendorId, modelId, vendorName, modelName))
240
241         # check whether this has already been registered at ffado.org
242         if not ffado.config.bypassdbus:
243             reg = ffado_registration(FFADO_VERSION, int(guid, 16),
244                                          vendorId, modelId,
245                                          vendorName, modelName)
246             reg.check_for_registration()
247
248             # The MOTU devices use unitVersion to differentiate models.  For
249             # the moment though we don't need to know precisely which model
250             # we're using beyond it being a pre-mark3 (modelId=0) or mark3
251             # (modelId=1) device.
252             if vendorId == 0x1f2:
253                 # All MOTU devices with a unit version of 0x15 or greater are
254                 # mark3 devices
255                 if (unitVersion >= 0x15):
256                     modelId = 0x00000001
257                 else:
258                     modelId = 0x00000000
259
260             # The RME devices use unitVersion to differentiate models.
261             # Therefore in the configuration file we use the config file's
262             # modelid field to store the unit version.  As a result we must
263             # override the modelId with the unit version here so the correct
264             # configuration file entry (and hense mixer widget) is identified.
265             if vendorId == 0xa35:
266                 modelId = unitVersion;
267
268         dev = self.devices.getDeviceById( vendorId, modelId )
269
270         w = QWidget( )
271         l = QVBoxLayout( w )
272
273         # create a control object
274         hw = ControlInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
275         clockselect = ClockSelectInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path )
276         samplerateselect = SamplerateSelectInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path )
277         streamingstatus = StreamingStatusInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path )
278         nickname = TextInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path+"/Generic/Nickname" )
279
280         #
281         # Generic elements for all mixers follow here:
282         #
283         globalmixer = GlobalMixer( w )
284         globalmixer.configrom = cfgrom
285         globalmixer.clockselect = clockselect
286         globalmixer.samplerateselect = samplerateselect
287         globalmixer.streamingstatus = streamingstatus
288         globalmixer.nickname = nickname
289         globalmixer.hw = hw
290         globalmixer.initValues()
291         l.addWidget( globalmixer, 1 )
292
293         #
294         # Line to separate
295         #
296         l.addWidget( HLine( w ) )
297
298         #
299         # Specific (or dummy) mixer widgets get loaded in the following
300         #
301         if 'mixer' in dev and dev['mixer'] != None:
302             mixerapp = dev['mixer']
303             global mixerwidget
304             exec( """
305 try:
306     import ffado.mixer.%s
307     globals()["mixerwidget"] = ffado.mixer.%s.%s( w )
308     found = True
309 except ImportError:
310     log.debug("bypassdbus set, %s module not available: ignored")
311     found = False
312 """ % (mixerapp.lower(), mixerapp.lower(), mixerapp, mixerapp.lower()) )
313         else:
314             found = False
315
316         if not found:
317             mixerwidget = Dummy( w )
318             mixerapp = modelName+" (Dummy)"
319
320         #
321         # The same for all mixers
322         #
323         l.addWidget( mixerwidget, 10 )
324         mixerwidget.configrom = cfgrom
325         mixerwidget.clockselect = clockselect
326         mixerwidget.samplerateselect = samplerateselect
327         mixerwidget.streamingstatus = streamingstatus
328         mixerwidget.nickname = nickname
329         mixerwidget.hw = hw
330         if 'buildMixer' in dir(mixerwidget):
331             mixerwidget.buildMixer()
332         if 'initValues' in dir(mixerwidget):
333             mixerwidget.initValues()
334         if 'getDisplayTitle' in dir(mixerwidget):
335             title = mixerwidget.getDisplayTitle()
336         else:
337             title = mixerapp
338
339         globalmixer.setName(title)
340         self.tabs.addTab( w, title )
341         self.panels[guid] = w
342
343         if 'onSamplerateChange' in dir(mixerwidget):
344           log.debug("Updating Mixer on samplerate change required")
345           globalmixer.onSamplerateChange = mixerwidget.onSamplerateChange
346
347         w.gmixSaveSetgs = globalmixer.saveSettings
348         w.gmixReadSetgs = globalmixer.readSettings
349         if 'saveSettings' in dir(mixerwidget):
350           w.smixSaveSetgs = mixerwidget.saveSettings
351           self.parent.saveaction.setEnabled(True)
352
353         if 'readSettings' in dir(mixerwidget):
354           w.smixReadSetgs = mixerwidget.readSettings
355           self.parent.openaction.setEnabled(True)
356
357     def displayPanels(self):
358         # if there is no panel, add the no-device message
359         # else make sure it is not present
360         if self.count() == 0:
361             self.tabs.hide()
362             self.tabs.setEnabled(False)
363             self.status.lblMessage.setText("No supported device found.")
364             self.status.show()
365             #self.statusBar().showMessage("No supported device found.", 5000)
366         else:
367             # Hide the status widget before showing the panel tab to prevent
368             # the panel manager's vertical size including that of the status
369             # widget.  For some reason, hiding the status after showing the
370             # tabs does not cause a recalculation of the panel manager's size,
371             # and the window ends up being larger than it needs to be.
372             self.status.hide()
373             self.tabs.show()
374             self.tabs.setEnabled(True)
375             #self.statusBar().showMessage("Configured the mixer for %i devices." % self.tabs.count())
376             if use_generic:
377                 #
378                 # Show the generic (development) mixer if it is available
379                 #
380                 w = GenericMixer( devmgr.bus, FFADO_DBUS_SERVER, mw )
381                 self.tabs.addTab( w, "Generic Mixer" )
382                 self.panels[GUID_GENERIC_MIXER] = w
383    
384     def updatePanels(self):
385         log.debug("PanelManager::updatePanels()")
386         nbDevices = self.devmgr.getNbDevices()
387         #self.statusBar().showMessage("Reconfiguring the mixer panels...")
388
389         # list of panels present
390         guids_with_tabs = self.panels.keys()
391
392         # build list of guids on the bus now
393         guids_present = []
394         guid_indexes = {}
395         for idx in range(nbDevices):
396             path = self.devmgr.getDeviceName(idx)
397             if ffado.config.bypassdbus:
398                 cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, path)
399             else:
400                 cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
401             guid = cfgrom.getGUID()
402             guids_present.append(guid)
403             guid_indexes[guid] = idx
404
405         # figure out what to remove
406         # the special panel (generic)
407         # that has (pseudo-)GUID 0
408         # is also automatically removed
409         to_remove = []
410         for guid in guids_with_tabs:
411             if not guid in guids_present:
412                 to_remove.append(guid)
413                 log.debug("going to remove %s" % str(guid))
414             else:
415                 log.debug("going to keep %s" % str(guid))
416
417         # figure out what to add
418         to_add = []
419         for guid in guids_present:
420             if not guid in guids_with_tabs:
421                 to_add.append(guid)
422                 log.debug("going to add %s" % str(guid))
423
424         # update the widget
425         for guid in to_remove:
426             self.removePanel(guid)
427
428         for guid in to_add:
429             # retrieve the device manager index
430             idx = guid_indexes[guid]
431             self.addPanel(idx)
432
433         self.displayPanels()
434
435     def refreshPanels(self):
436         log.debug("PanelManager::refreshPanels()")
437         nbDevices = self.devmgr.getNbDevices()
438         #self.statusBar().showMessage("Reconfiguring the mixer panels...")
439
440         # list of panels present
441         guids_with_tabs = self.panels.keys()
442
443         # build list of guids on the bus now
444         guid_indexes = {}
445         for idx in range(nbDevices):
446             path = self.devmgr.getDeviceName(idx)
447             cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
448             guid = cfgrom.getGUID()
449             guid_indexes[guid] = idx
450
451         # remove/create the widget
452         for guid in guids_with_tabs:
453             self.removePanel(guid)
454             idx = guid_indexes[guid]
455             self.addPanel(idx)
456
457         self.displayPanels()
458
459     def saveSettings(self):
460         saveString = []
461         saveString.append('<?xml version="1.0" encoding="UTF-8"?>\n')
462         saveString.append('<fileversion>\n')
463         saveString.append('  <major>\n')
464         saveString.append('    ' + str(FILE_VERSION).split('.')[0] + '\n')
465         saveString.append('  </major>\n')
466         saveString.append('  <minor>\n')
467         saveString.append('    ' + str(FILE_VERSION).split('.')[1] + '\n')
468         saveString.append('  </minor>\n')
469         saveString.append('</fileversion>\n')
470         saveString.append('<ffadoversion>\n')
471         saveString.append('  <major>\n')
472         saveString.append('    ' + str(str(FFADO_VERSION).split('-')[0]).split('.')[0] + '\n')
473         saveString.append('  </major>\n')
474         saveString.append('  <minor>\n')
475         saveString.append('    ' + str(str(FFADO_VERSION).split('-')[0]).split('.')[1] + '\n')
476         saveString.append('  </minor>\n')
477         saveString.append('</ffadoversion>\n')
478         for guid in self.panels.keys():
479           saveString.append('<device>\n')
480           saveString.append('  <guid>\n')
481           saveString.append('    ' + str(guid) + '\n')
482           saveString.append('  </guid>\n')       
483           w = self.panels[guid]
484           indent = "  "
485           saveString.extend(w.gmixSaveSetgs(indent))
486           if 'smixSaveSetgs' in dir(w):
487               saveString.extend(w.smixSaveSetgs(indent))
488           saveString.append('</device>\n')
489         # file saving
490         savefilename = QFileDialog.getSaveFileName(self, 'Save File', os.getenv('HOME'))
491         try:
492           f = open(savefilename, 'w')
493         except IOError:
494           print "Unable to open save file"
495           return
496         for s in saveString:
497           f.write(s)
498         f.close()
499
500     def readSettings(self):
501         readfilename = QFileDialog.getOpenFileName(self, 'Open File', os.getenv('HOME'))
502         try:
503           f = open(readfilename, 'r')
504         except IOError:
505           print "Unable to open file"
506           return
507         log.debug("Opening file %s" % readfilename)
508         # discard useless whitespace characters
509         readString = []
510         for line in f:
511           readString.append(" ".join(str(line).split()))
512         f.close()
513         # Check it is a compatible "FFADO" file
514         # It must start with the <?xml ... tag as the first string
515         if readString[0].find("<?xml") == -1:
516             print "Not an xml data file"
517             return
518         # Then there must be a file version tag somewhere in the file
519         try:
520             idx = readString.index('<fileversion>')
521         except Exception:
522             print "Data file should contain the version tag"
523             return
524         if readString[idx+1].find("<major>") == -1:
525             print "Incompatible versioning of the file"
526         if readString[idx+3].find("</major>") == -1:
527             print "Not a valid xml file"
528         if readString[idx+4].find("<minor>") == -1:
529             print "Incompatible versioning of the file"
530         if readString[idx+6].find("</minor>") == -1:
531             print "Not a valid xml file"
532         version_major = readString[idx+2]
533         version =  version_major + '.' + readString[idx+5]
534         log.debug("File version: %s" % version)
535         # File version newer than present
536         if int(version_major) > int(str(FILE_VERSION).split('.')[0]):
537             print "File version is too recent: you should upgrade your FFADO installation"
538             return
539         # FIXME At a time it will be necessary to detect if an older major version is detected
540         #
541         # It looks like useless to check for the FFADO version
542         # Add something here if you would like so
543         #
544         # Now search for devices
545         nd = readString.count('<device>');
546         n  = readString.count('</device>');
547         if n != nd:
548             print "Not a regular xml file: opening device tag must match closing ones"
549             return
550         while nd > 0:
551           idxb = readString.index('<device>')
552           idxe = readString.index('</device>')
553           if idxe < idxb+1:
554             print "Not a regular xml file: data must be enclosed between a <device> and </device> tag"
555             return
556           stringDev = []
557           for s in readString[idxb:idxe]:
558             stringDev.append(s)
559           # determine the device guid
560           try:
561               idx = stringDev.index('<guid>')
562           except Exception:
563               print "Device guid not found"
564               return
565           guid = stringDev[idx+1]
566           log.debug("Device %s found" % guid)
567
568           if guid in self.panels:
569               w = self.panels[guid]
570               w.gmixReadSetgs(stringDev)
571               if 'smixReadSetgs' in dir(w):
572                 w.smixReadSetgs(stringDev)
573               log.debug("Settings changed for device %s" % guid)
574           else:
575               log.debug("Device %s not present; settings ignored" % guid)
576
577           del stringDev[:]
578           del readString[idxb:idxe]
579           nd -= 1
580    
581 # vim: et
Note: See TracBrowser for help on using the browser.