root/trunk/libffado/src/bebob/bebob_avplug.cpp

Revision 742, 21.7 kB (checked in by ppalmers, 13 years ago)

- Remove some obsolete support files and dirs

- Clean up the license statements in the source files. Everything is

GPL version 3 now.

- Add license and copyright notices to scons scripts

- Clean up some other text files

Line 
1 /*
2  * Copyright (C) 2005-2007 by Daniel Wagner
3  *
4  * This file is part of FFADO
5  * FFADO = Free Firewire (pro-)audio drivers for linux
6  *
7  * FFADO is based upon FreeBoB
8  *
9  * This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  */
23
24 #include "bebob/bebob_avplug.h"
25 #include "bebob/bebob_avdevice.h"
26 #include "libieee1394/configrom.h"
27
28 #include "libieee1394/ieee1394service.h"
29 #include "libutil/cmd_serialize.h"
30
31 #include <sstream>
32
33 using namespace AVC;
34
35 namespace BeBoB {
36
37 Plug::Plug( AVC::Unit* unit,
38             AVC::Subunit* subunit,
39             AVC::function_block_type_t functionBlockType,
40             AVC::function_block_type_t functionBlockId,
41             AVC::Plug::EPlugAddressType plugAddressType,
42             AVC::Plug::EPlugDirection plugDirection,
43             AVC::plug_id_t plugId )
44     : AVC::Plug( unit,
45                  subunit,
46                  functionBlockType,
47                  functionBlockId,
48                  plugAddressType,
49                  plugDirection,
50                  plugId )
51 {
52     debugOutput( DEBUG_LEVEL_VERBOSE,
53                  "nodeId = %d, subunitType = %d, "
54                  "subunitId = %d, functionBlockType = %d, "
55                  "functionBlockId = %d, addressType = %d, "
56                  "direction = %d, id = %d\n",
57                  unit->getConfigRom().getNodeId(),
58                  getSubunitType(),
59                  getSubunitId(),
60                  functionBlockType,
61                  functionBlockId,
62                  plugAddressType,
63                  plugDirection,
64                  plugId );
65 }
66
67 Plug::Plug( const Plug& rhs )
68     : AVC::Plug( rhs )
69 {
70
71 }
72
73 Plug::Plug()
74     : AVC::Plug()
75 {
76 }
77
78 Plug::~Plug()
79 {
80
81 }
82
83 bool
84 Plug::discover()
85 {
86     if ( !discoverPlugType() ) {
87         debugError( "discover: Could not discover plug type (%d,%d,%d,%d,%d)\n",
88                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
89         return false;
90     }
91
92     if ( !discoverName() ) {
93         debugError( "Could not discover name (%d,%d,%d,%d,%d)\n",
94                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
95         return false;
96     }
97
98     if ( !discoverNoOfChannels() ) {
99         debugError( "Could not discover number of channels "
100                     "(%d,%d,%d,%d,%d)\n",
101                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
102         return false;
103     }
104
105     if ( !discoverChannelPosition() ) {
106         debugError( "Could not discover channel positions "
107                     "(%d,%d,%d,%d,%d)\n",
108                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
109         return false;
110     }
111
112     if ( !discoverChannelName() ) {
113         debugError( "Could not discover channel name "
114                     "(%d,%d,%d,%d,%d)\n",
115                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
116         return false;
117     }
118
119     if ( !discoverClusterInfo() ) {
120         debugError( "Could not discover channel name "
121                     "(%d,%d,%d,%d,%d)\n",
122                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
123         return false;
124     }
125
126     if ( !discoverStreamFormat() ) {
127         debugError( "Could not discover stream format "
128                     "(%d,%d,%d,%d,%d)\n",
129                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
130         return false;
131     }
132
133     if ( !discoverSupportedStreamFormats() ) {
134         debugError( "Could not discover supported stream formats "
135                     "(%d,%d,%d,%d,%d)\n",
136                     m_unit->getConfigRom().getNodeId(), getSubunitType(), getSubunitId(), m_direction, m_id );
137         return false;
138     }
139
140     return m_unit->getPlugManager().addPlug( *this );
141 }
142
143 bool
144 Plug::discoverConnections()
145 {
146     return discoverConnectionsInput() && discoverConnectionsOutput();
147 }
148
149 bool
150 Plug::discoverPlugType()
151 {
152     ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
153     ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
154         ExtendedPlugInfoInfoType::eIT_PlugType );
155     extendedPlugInfoInfoType.initialize();
156     extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
157     extPlugInfoCmd.setVerbose( getDebugLevel() );
158
159     if ( !extPlugInfoCmd.fire() ) {
160         debugError( "plug type command failed\n" );
161         return false;
162     }
163
164     m_infoPlugType = eAPT_Unknown;
165
166     if ( extPlugInfoCmd.getResponse() == AVCCommand::eR_Implemented ) {
167
168         ExtendedPlugInfoInfoType* infoType = extPlugInfoCmd.getInfoType();
169         if ( infoType
170              && infoType->m_plugType )
171         {
172             plug_type_t plugType = infoType->m_plugType->m_plugType;
173
174             debugOutput( DEBUG_LEVEL_VERBOSE,
175                          "plug %d is of type %d (%s)\n",
176                          m_id,
177                          plugType,
178                          extendedPlugInfoPlugTypeToString( plugType ) );
179             switch ( plugType ) {
180             case ExtendedPlugInfoPlugTypeSpecificData::eEPIPT_IsoStream:
181                 m_infoPlugType = eAPT_IsoStream;
182                 break;
183             case ExtendedPlugInfoPlugTypeSpecificData::eEPIPT_AsyncStream:
184                 m_infoPlugType = eAPT_AsyncStream;
185                 break;
186             case ExtendedPlugInfoPlugTypeSpecificData::eEPIPT_Midi:
187                 m_infoPlugType = eAPT_Midi;
188                 break;
189             case ExtendedPlugInfoPlugTypeSpecificData::eEPIPT_Sync:
190                 m_infoPlugType = eAPT_Sync;
191                 break;
192             case ExtendedPlugInfoPlugTypeSpecificData::eEPIPT_Analog:
193                 m_infoPlugType = eAPT_Analog;
194                 break;
195             case ExtendedPlugInfoPlugTypeSpecificData::eEPIPT_Digital:
196                 m_infoPlugType = eAPT_Digital;
197                 break;
198             default:
199                 m_infoPlugType = eAPT_Unknown;
200
201             }
202         }
203     } else {
204         debugError( "Plug does not implement extended plug info plug "
205                     "type info command\n" );
206         return false;
207     }
208
209    return true;
210 }
211
212 bool
213 Plug::discoverName()
214 {
215     ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
216     ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
217         ExtendedPlugInfoInfoType::eIT_PlugName );
218     extendedPlugInfoInfoType.initialize();
219     extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
220     extPlugInfoCmd.setVerbose( getDebugLevel() );
221
222     if ( !extPlugInfoCmd.fire() ) {
223         debugError( "name command failed\n" );
224         return false;
225     }
226
227     ExtendedPlugInfoInfoType* infoType = extPlugInfoCmd.getInfoType();
228     if ( infoType
229          && infoType->m_plugName )
230     {
231         std::string name =
232             infoType->m_plugName->m_name;
233
234         debugOutput( DEBUG_LEVEL_VERBOSE,
235                      "plug %d has name '%s'\n",
236                      m_id,
237                      name.c_str() );
238
239         m_name = name;
240     }
241     return true;
242 }
243
244 bool
245 Plug::discoverNoOfChannels()
246 {
247     ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
248     //extPlugInfoCmd.setVerbose( true );
249     ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
250         ExtendedPlugInfoInfoType::eIT_NoOfChannels );
251     extendedPlugInfoInfoType.initialize();
252     extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
253     extPlugInfoCmd.setVerbose( getDebugLevel() );
254
255     if ( !extPlugInfoCmd.fire() ) {
256         debugError( "number of channels command failed\n" );
257         return false;
258     }
259
260     ExtendedPlugInfoInfoType* infoType = extPlugInfoCmd.getInfoType();
261     if ( infoType
262          && infoType->m_plugNrOfChns )
263     {
264         nr_of_channels_t nrOfChannels
265             = infoType->m_plugNrOfChns->m_nrOfChannels;
266
267         debugOutput( DEBUG_LEVEL_VERBOSE,
268                      "plug %d has %d channels\n",
269                      m_id,
270                      nrOfChannels );
271
272         m_nrOfChannels = nrOfChannels;
273     }
274     return true;
275 }
276
277 bool
278 Plug::discoverChannelPosition()
279 {
280     ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
281     ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
282         ExtendedPlugInfoInfoType::eIT_ChannelPosition );
283     extendedPlugInfoInfoType.initialize();
284     extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
285     extPlugInfoCmd.setVerbose( getDebugLevel() );
286
287     if ( !extPlugInfoCmd.fire() ) {
288         debugError( "channel position command failed\n" );
289         return false;
290     }
291
292     ExtendedPlugInfoInfoType* infoType = extPlugInfoCmd.getInfoType();
293     if ( infoType
294          && infoType->m_plugChannelPosition )
295     {
296         if ( !copyClusterInfo( *( infoType->m_plugChannelPosition ) ) ) {
297             debugError( "Could not copy channel position "
298                         "information\n" );
299             return false;
300         }
301
302         debugOutput( DEBUG_LEVEL_VERBOSE,
303                      "plug %d: channel position information "
304                      "retrieved\n",
305                      m_id );
306
307         debugOutputClusterInfos( DEBUG_LEVEL_VERBOSE );
308     }
309
310     return true;
311 }
312
313 bool
314 Plug::copyClusterInfo(ExtendedPlugInfoPlugChannelPositionSpecificData&
315                         channelPositionData )
316 {
317     int index = 1;
318     for ( ExtendedPlugInfoPlugChannelPositionSpecificData::ClusterInfoVector::const_iterator it
319               = channelPositionData.m_clusterInfos.begin();
320           it != channelPositionData.m_clusterInfos.end();
321           ++it )
322     {
323         const ExtendedPlugInfoPlugChannelPositionSpecificData::ClusterInfo*
324             extPlugSpClusterInfo = &( *it );
325
326         ClusterInfo clusterInfo;
327         clusterInfo.m_nrOfChannels = extPlugSpClusterInfo->m_nrOfChannels;
328         clusterInfo.m_index = index;
329         index++;
330
331         for (  ExtendedPlugInfoPlugChannelPositionSpecificData::ChannelInfoVector::const_iterator cit
332                   = extPlugSpClusterInfo->m_channelInfos.begin();
333               cit != extPlugSpClusterInfo->m_channelInfos.end();
334               ++cit )
335         {
336             const ExtendedPlugInfoPlugChannelPositionSpecificData::ChannelInfo*
337                 extPlugSpChannelInfo = &( *cit );
338
339             ChannelInfo channelInfo;
340             channelInfo.m_streamPosition =
341                 extPlugSpChannelInfo->m_streamPosition-1;
342             // FIXME: this can only become a mess with the two meanings
343             //        of the location parameter. the audio style meaning
344             //        starts from 1, the midi style meaning from 0
345             //        lucky for us we recalculate this for the midi channels
346             //        and don't use this value.
347             channelInfo.m_location =
348                 extPlugSpChannelInfo->m_location;
349
350             clusterInfo.m_channelInfos.push_back( channelInfo );
351         }
352         m_clusterInfos.push_back( clusterInfo );
353     }
354
355     return true;
356 }
357
358 bool
359 Plug::discoverChannelName()
360 {
361     for ( ClusterInfoVector::iterator clit = m_clusterInfos.begin();
362           clit != m_clusterInfos.end();
363           ++clit )
364     {
365         ClusterInfo* clitInfo = &*clit;
366
367         for ( ChannelInfoVector::iterator pit =  clitInfo->m_channelInfos.begin();
368               pit != clitInfo->m_channelInfos.end();
369               ++pit )
370         {
371             ChannelInfo* channelInfo = &*pit;
372
373             ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
374             ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
375                 ExtendedPlugInfoInfoType::eIT_ChannelName );
376             extendedPlugInfoInfoType.initialize();
377             extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
378             extPlugInfoCmd.setVerbose( getDebugLevel() );
379
380             ExtendedPlugInfoInfoType* infoType =
381                 extPlugInfoCmd.getInfoType();
382             if ( infoType ) {
383                 infoType->m_plugChannelName->m_streamPosition =
384                     channelInfo->m_streamPosition + 1;
385             }
386             if ( !extPlugInfoCmd.fire() ) {
387                 debugError( "channel name command failed\n" );
388                 return false;
389             }
390             infoType = extPlugInfoCmd.getInfoType();
391             if ( infoType
392                  && infoType->m_plugChannelName )
393             {
394                 debugOutput( DEBUG_LEVEL_VERBOSE,
395                              "plug %d stream "
396                              "position %d: channel name = %s\n",
397                              m_id,
398                              channelInfo->m_streamPosition,
399                              infoType->m_plugChannelName->m_plugChannelName.c_str() );
400                 channelInfo->m_name =
401                     infoType->m_plugChannelName->m_plugChannelName;
402             }
403
404         }
405     }
406
407     return true;
408 }
409
410 bool
411 Plug::discoverClusterInfo()
412 {
413     if ( m_infoPlugType == eAPT_Sync )
414     {
415         // If the plug is of type sync it is either a normal 2 channel
416         // stream (not compound stream) or it is a compound stream
417         // with exactly one cluster. This depends on the
418         // extended stream format command version which is used.
419         // We are not interested in this plug so we skip it.
420         debugOutput( DEBUG_LEVEL_VERBOSE,
421                      "%s plug %d is of type sync -> skip\n",
422                      getName(),
423                      m_id );
424         return true;
425     }
426
427     for ( ClusterInfoVector::iterator clit = m_clusterInfos.begin();
428           clit != m_clusterInfos.end();
429           ++clit )
430     {
431         ClusterInfo* clusterInfo = &*clit;
432
433         ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
434         ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
435             ExtendedPlugInfoInfoType::eIT_ClusterInfo );
436         extendedPlugInfoInfoType.initialize();
437         extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
438         extPlugInfoCmd.setVerbose( getDebugLevel() );
439
440         extPlugInfoCmd.getInfoType()->m_plugClusterInfo->m_clusterIndex =
441             clusterInfo->m_index;
442
443         if ( !extPlugInfoCmd.fire() ) {
444             debugError( "cluster info command failed\n" );
445             return false;
446         }
447
448         ExtendedPlugInfoInfoType* infoType = extPlugInfoCmd.getInfoType();
449         if ( infoType
450              && infoType->m_plugClusterInfo )
451         {
452             debugOutput( DEBUG_LEVEL_VERBOSE,
453                          "%s plug %d: cluster index = %d, "
454                          "portType %s, cluster name = %s\n",
455                          getName(),
456                          m_id,
457                          infoType->m_plugClusterInfo->m_clusterIndex,
458                          extendedPlugInfoClusterInfoPortTypeToString(
459                              infoType->m_plugClusterInfo->m_portType ),
460                          infoType->m_plugClusterInfo->m_clusterName.c_str() );
461
462             clusterInfo->m_portType = infoType->m_plugClusterInfo->m_portType;
463             clusterInfo->m_name = infoType->m_plugClusterInfo->m_clusterName;
464         }
465     }
466
467     return true;
468 }
469
470 bool
471 Plug::discoverConnectionsInput()
472 {
473     ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
474     ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
475         ExtendedPlugInfoInfoType::eIT_PlugInput );
476     extendedPlugInfoInfoType.initialize();
477     extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
478     extPlugInfoCmd.setVerbose( getDebugLevel() );
479
480     if ( !extPlugInfoCmd.fire() ) {
481         debugError( "plug type command failed\n" );
482         return false;
483     }
484
485     if ( extPlugInfoCmd.getResponse() == AVCCommand::eR_Rejected ) {
486         // Plugs does not like to be asked about connections
487         debugOutput( DEBUG_LEVEL_VERBOSE, "Plug '%s' rejects "
488                      "connections command\n",
489                      getName() );
490         return true;
491     }
492
493     ExtendedPlugInfoInfoType* infoType = extPlugInfoCmd.getInfoType();
494     if ( infoType
495          && infoType->m_plugInput )
496     {
497         PlugAddressSpecificData* plugAddress
498             = infoType->m_plugInput->m_plugAddress;
499
500         if ( plugAddress->m_addressMode ==
501              PlugAddressSpecificData::ePAM_Undefined )
502         {
503             // This plug has no input connection
504             return true;
505         }
506
507         if ( !discoverConnectionsFromSpecificData( eAPD_Input,
508                                                    plugAddress,
509                                                    m_inputConnections ) )
510         {
511             debugWarning( "Could not discover connnections for plug '%s'\n",
512                           getName() );
513         }
514     } else {
515         debugError( "no valid info type for plug '%s'\n", getName() );
516         return false;
517     }
518
519     return true;
520 }
521
522 bool
523 Plug::discoverConnectionsOutput()
524 {
525     ExtendedPlugInfoCmd extPlugInfoCmd = setPlugAddrToPlugInfoCmd();
526     ExtendedPlugInfoInfoType extendedPlugInfoInfoType(
527         ExtendedPlugInfoInfoType::eIT_PlugOutput );
528     extendedPlugInfoInfoType.initialize();
529     extPlugInfoCmd.setInfoType( extendedPlugInfoInfoType );
530     extPlugInfoCmd.setVerbose( getDebugLevel() );
531
532     if ( !extPlugInfoCmd.fire() ) {
533         debugError( "plug type command failed\n" );
534         return false;
535     }
536
537     if ( extPlugInfoCmd.getResponse() == AVCCommand::eR_Rejected ) {
538         // Plugs does not like to be asked about connections
539         debugOutput( DEBUG_LEVEL_VERBOSE, "Plug '%s' rejects "
540                      "connections command\n",
541                      getName() );
542         return true;
543     }
544
545     ExtendedPlugInfoInfoType* infoType = extPlugInfoCmd.getInfoType();
546     if ( infoType
547          && infoType->m_plugOutput )
548     {
549         if ( infoType->m_plugOutput->m_nrOfOutputPlugs
550              != infoType->m_plugOutput->m_outputPlugAddresses.size() )
551         {
552             debugError( "number of output plugs (%d) disagree with "
553                         "number of elements in plug address vector (%d)\n",
554                         infoType->m_plugOutput->m_nrOfOutputPlugs,
555                         infoType->m_plugOutput->m_outputPlugAddresses.size());
556         }
557
558         if ( infoType->m_plugOutput->m_nrOfOutputPlugs == 0 ) {
559             // This plug has no output connections
560             return true;
561         }
562
563         for ( unsigned int i = 0;
564               i < infoType->m_plugOutput->m_outputPlugAddresses.size();
565               ++i )
566         {
567             PlugAddressSpecificData* plugAddress
568                 = infoType->m_plugOutput->m_outputPlugAddresses[i];
569
570             if ( !discoverConnectionsFromSpecificData( eAPD_Output,
571                                                        plugAddress,
572                                                        m_outputConnections ) )
573             {
574                 debugWarning( "Could not discover connnections for "
575                               "plug '%s'\n", getName() );
576             }
577         }
578     } else {
579         debugError( "no valid info type for plug '%s'\n", getName() );
580         return false;
581     }
582
583     return true;
584 }
585
586 ExtendedPlugInfoCmd
587 Plug::setPlugAddrToPlugInfoCmd()
588 {
589     ExtendedPlugInfoCmd extPlugInfoCmd( m_unit->get1394Service() );
590
591     switch( getSubunitType() ) {
592     case eST_Unit:
593         {
594             UnitPlugAddress::EPlugType ePlugType =
595                 UnitPlugAddress::ePT_Unknown;
596             switch ( m_addressType ) {
597                 case eAPA_PCR:
598                     ePlugType = UnitPlugAddress::ePT_PCR;
599                     break;
600                 case eAPA_ExternalPlug:
601                     ePlugType = UnitPlugAddress::ePT_ExternalPlug;
602                     break;
603                 case eAPA_AsynchronousPlug:
604                     ePlugType = UnitPlugAddress::ePT_AsynchronousPlug;
605                     break;
606                 default:
607                     ePlugType = UnitPlugAddress::ePT_Unknown;
608             }
609             UnitPlugAddress unitPlugAddress( ePlugType,
610                                              m_id );
611             extPlugInfoCmd.setPlugAddress(
612                 PlugAddress( convertPlugDirection( getPlugDirection() ),
613                              PlugAddress::ePAM_Unit,
614                              unitPlugAddress ) );
615         }
616         break;
617     case eST_Music:
618     case eST_Audio:
619         {
620             switch( m_addressType ) {
621             case eAPA_SubunitPlug:
622             {
623                 SubunitPlugAddress subunitPlugAddress( m_id );
624                 extPlugInfoCmd.setPlugAddress(
625                     PlugAddress(
626                         convertPlugDirection( getPlugDirection() ),
627                         PlugAddress::ePAM_Subunit,
628                         subunitPlugAddress ) );
629             }
630             break;
631             case eAPA_FunctionBlockPlug:
632             {
633                 FunctionBlockPlugAddress functionBlockPlugAddress(
634                     m_functionBlockType,
635                     m_functionBlockId,
636                     m_id );
637                 extPlugInfoCmd.setPlugAddress(
638                     PlugAddress(
639                         convertPlugDirection( getPlugDirection() ),
640                         PlugAddress::ePAM_FunctionBlock,
641                         functionBlockPlugAddress ) );
642             }
643             break;
644             default:
645                 extPlugInfoCmd.setPlugAddress(PlugAddress());
646             }
647         }
648         break;
649     default:
650         debugError( "Unknown subunit type\n" );
651     }
652
653     extPlugInfoCmd.setNodeId( m_unit->getConfigRom().getNodeId() );
654     extPlugInfoCmd.setCommandType( AVCCommand::eCT_Status );
655     extPlugInfoCmd.setSubunitId( getSubunitId() );
656     extPlugInfoCmd.setSubunitType( getSubunitType() );
657
658     return extPlugInfoCmd;
659 }
660
661 }
Note: See TracBrowser for help on using the browser.