A Python UPnP Media Server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1086 lines
28 KiB

  1. #!/usr/bin/env python
  2. from twisted.internet.protocol import BaseProtocol, Protocol, ConnectedDatagramProtocol
  3. from twisted.internet.serialport import SerialPort
  4. from twisted.internet import defer, reactor, threads
  5. from zope.interface import implements, Interface, Attribute
  6. import array
  7. import cddb
  8. import serial
  9. import sys
  10. import time
  11. class ICDPlayerTransport(Interface):
  12. power = Attribute('A C{bool}')
  13. playing = Attribute('A C{bool}')
  14. paused = Attribute('A C{bool}')
  15. dooropen = Attribute('A C{bool}')
  16. discknown = Attribute('A C{bool}')
  17. curdisc = Attribute('A C{int} of the current disc loaded or None.')
  18. alldiscs = Attribute('A C{bool}, does playback include all discs.')
  19. repeateall = Attribute('A C{bool}')
  20. repeateone = Attribute('A C{bool}')
  21. shuffle = Attribute('A C{bool}')
  22. program = Attribute('False or 1, 2 or 3')
  23. cdnum = Attribute('A number that is the CD player ID')
  24. disccount = Attribute('A count of the capacity of the player.')
  25. model = Attribute('The model.')
  26. def play():
  27. '''Start playing at current possition.'''
  28. def stop():
  29. '''Stop playing.'''
  30. def pause():
  31. '''Pause.'''
  32. def togglepause():
  33. '''Toggle Paused state. Will trigger the respective paused
  34. or playing on the protocol.'''
  35. def nexttrack():
  36. '''Next Track.'''
  37. def previoustrack():
  38. '''Previous Track.'''
  39. def poweron():
  40. '''Turn power on. Returns a defered that will be triggered
  41. when power is on.'''
  42. def poweroff():
  43. '''Turn power off.'''
  44. def discinfo(d):
  45. '''Report disc info, will stop playback. Returns Deferred.'''
  46. def trackinfo(d, t):
  47. '''Report track info, will stop playback. Returns Deferred.'''
  48. def playdisc(disc, track=1):
  49. '''Start playing disc. Returns Deferred.'''
  50. def cuedisc(disc, track=1):
  51. '''Cue disc to track. Returns Deferred.'''
  52. def loaddisc(disc, track=1, load=True):
  53. '''Make sure disc is loaded, cue if necessary. Returns a
  54. Deferred. If load is False, will not load the disc. The
  55. Deferred will return True if the disc is loaded, otherwise
  56. False.'''
  57. def discs():
  58. '''Return set of discs present, or None if the discs are
  59. unknown at this time.'''
  60. def __contains__(k):
  61. '''Check if a disc is present in the changer.'''
  62. def __len__(k):
  63. '''Number of discs present in the changer. This is not the
  64. capacitity..'''
  65. def __iter__():
  66. '''Iterate over all the discs in the changer.'''
  67. def setcontinuous(alldiscs=True):
  68. '''Set continuous playback, if alldiscs is False, one disc.'''
  69. def setshuffle(alldiscs=True):
  70. '''Set shuffle playback, if alldiscs is False, one disc.'''
  71. def settimeout(timeout=300):
  72. '''After timeout seconds of no commands being issues, and
  73. the player not playing discs, power off.'''
  74. def getcddbid(disc):
  75. '''Return CDDB id of disc.'''
  76. class CDPlayerProtocol(BaseProtocol):
  77. '''This protocol is a bit different in that the transport implements
  78. the CDPlayerTransport interface.'''
  79. def stateChanged(self):
  80. # one of the attributes changes
  81. pass
  82. class SLinkException(Exception):
  83. pass
  84. class DiscError(SLinkException):
  85. def __init__(self, disc, s):
  86. SLinkException.__init__(self, s % disc)
  87. self.disc = disc
  88. class NoDisc(SLinkException):
  89. pass
  90. # Not really BaseProtocol
  91. class SLinkCDTransport(object):
  92. implements(ICDPlayerTransport)
  93. def __init__(self, sprotocol, cdnum):
  94. if cdnum not in (1, 2, 3):
  95. raise ValueError('invalid CD: %d' % cdnum)
  96. self._proto = sprotocol
  97. self._cdnum = cdnum
  98. self._childprotocol = None
  99. # Internal state variables
  100. self._discs = None
  101. self._discpos = None
  102. self._disccount = None
  103. self.loadeddisc = None
  104. self._responses = {}
  105. def makeConnection(self, protocol):
  106. print 'mC'
  107. self._childprotocol = protocol
  108. # We can now start issuing commands to _proto, our parent.
  109. # Once we have gathered all the state we need, we will
  110. # connect to our child protocol.
  111. l = []
  112. d = self.runCommand('queryplayercap', 'decksize')
  113. d.addCallback(lambda x: self.printargs('c'))
  114. l.append(d)
  115. d = self.runCommand('querydeckmodel', 'deckmodel')
  116. d.addCallback(self.processdeckmodel)
  117. d.addCallback(lambda x: self.printargs('b'))
  118. l.append(d)
  119. d = self.runCommand('querystatus', 'status')
  120. d.addCallback(lambda x: self.printargs('after querystatus done'))
  121. l.append(d)
  122. d = defer.DeferredList(l)
  123. d.addCallback(lambda x: self.printargs('here'))
  124. d.addCallback(lambda x, p=protocol, s=self: p.makeConnection(s))
  125. def printargs(self, args):
  126. print 'mpa:', `args`
  127. def processdeckmodel(self, args):
  128. self._model = args[0]
  129. def processstatus(self, args):
  130. a = array.array('B', args[0])
  131. f = a[0]
  132. r = None
  133. if f & 0x10:
  134. self._power = False
  135. else:
  136. self._power = True
  137. if f & 0x2f == 0x2f:
  138. self._dooropen = True
  139. self._playing = False
  140. self._paused = False
  141. else:
  142. self._dooropen = False
  143. if f & 0x1:
  144. self._playing = True
  145. else:
  146. self._playing = False
  147. if f & 0x2:
  148. self._paused = True
  149. else:
  150. self._paused = False
  151. f = a[1]
  152. if f & 0x80:
  153. self._discknown = True
  154. if self._discs is None:
  155. r = self.getdiscs()
  156. else:
  157. self._discknown = False
  158. #self._discs = None
  159. if self._power is True:
  160. # We don't know discs yet, try again in a few
  161. # seconds.
  162. r = defer.Deferred()
  163. reactor.callLater(1, lambda:
  164. self.runCommand('querystatus',
  165. 'status').chainDeferred(r))
  166. if f & 0x40:
  167. self._alldiscs = True
  168. else:
  169. self._alldiscs = False
  170. if f & 0x10:
  171. self._repeateall = True
  172. self._repeateone = False
  173. elif f & 0x20:
  174. self._repeateall = False
  175. self._repeateone = True
  176. else:
  177. self._repeateall = False
  178. self._repeateone = False
  179. if f & 0xf == 0x1:
  180. self._shuffle = True
  181. self._program = False
  182. elif f & 0xf in (0x4, 0x5, 0x6):
  183. self._shuffle = False
  184. self._program = (f & 0xf) - 0x3
  185. else:
  186. self._shuffle = False
  187. self._program = False
  188. print 'self:', `self`, `r`
  189. return r
  190. def __repr__(self):
  191. return '<SLinkCDTransport: %s>' % ', '.join('%s=%s' % (k, `getattr(self, k)`) for k in ( 'power', 'playing', 'paused', 'dooropen', 'discknown', 'alldiscs', 'repeateall', 'repeateone', 'shuffle', 'program', 'disccount', 'model', ))
  192. def getdiscs(self):
  193. print 'gd called'
  194. self._discsnew = set()
  195. l = self.runCommand('querydiscs', [ 'disc_list%d' % i for i in xrange((self.disccount + 103) / 104) ])
  196. for i, d in enumerate(l):
  197. d.addCallback(lambda x, i=i: self.processdisclist(i, x))
  198. r = defer.DeferredList(l)
  199. r.addCallback(self.freezediscs)
  200. return r
  201. def freezediscs(self, args):
  202. print 'fd called'
  203. self._discs = frozenset(self._discsnew)
  204. self._discsnew = None
  205. bits = dict((1 << i, i) for i in xrange(8))
  206. def processdisclist(self, part, args):
  207. off = part
  208. a = array.array('B', args[0])
  209. for pos, i in enumerate(a):
  210. while i:
  211. b = i & -i
  212. bitpos = self.bits[b]
  213. i ^= b
  214. self._discsnew.add(104 * off + pos * 8 +
  215. bitpos + 1)
  216. def insertresponse(self, resp, obj):
  217. if resp in self._responses:
  218. self._responses[resp].append(obj)
  219. else:
  220. self._responses[resp] = [ obj ]
  221. def runCommand(self, cmd, response, *args, **kwargs):
  222. for i in kwargs.pop('ignoreresponses', []):
  223. d = defer.Deferred()
  224. self.insertresponse(i, d)
  225. if isinstance(response, list):
  226. d = []
  227. for i in response:
  228. dd = defer.Deferred()
  229. self.insertresponse(i, dd)
  230. d.append(dd)
  231. else:
  232. d = defer.Deferred()
  233. self.insertresponse(response, d)
  234. self._proto.sendmsg(cmd, self._cdnum, *args)
  235. return d
  236. def gotResponse(self, cmd, *args):
  237. # Got a response from a command
  238. expected = True
  239. print 'gR:', `cmd`, `args`
  240. # A few commands are state change, so handle them here.
  241. r = None
  242. if cmd in ('nowat', 'tocread', 'trackinfo', 'track_info', 'discinfo', ):
  243. self._discpos = args[0]
  244. elif cmd == 'poweron':
  245. print 'poweron, doing querystatus'
  246. r = self.runCommand('querystatus', 'status')
  247. elif cmd == 'poweroff':
  248. self._discpos = None
  249. r = self.runCommand('querystatus', 'status')
  250. elif cmd == 'decksize':
  251. if self._disccount is not None and self._disccount != \
  252. args[0]:
  253. # XXX state changed!
  254. pass
  255. self._disccount = args[0]
  256. elif cmd == 'status':
  257. r = self.processstatus(args)
  258. #elif cmd == 0x18
  259. # self._discs = None
  260. else:
  261. expected = False
  262. if r is not None:
  263. r.addCallback(lambda x: self.finishResponse(cmd,
  264. expected, *args))
  265. return
  266. self.finishResponse(cmd, expected, *args)
  267. def finishResponse(self, cmd, expected, *args):
  268. if cmd not in self._responses:
  269. if not expected:
  270. print 'Unexpected response:', `cmd`, `args`
  271. return
  272. # We pop so that a future command will be scheduled properly
  273. for i in self._responses.pop(cmd):
  274. i.callback(args)
  275. power = property(lambda x: x._power)
  276. playing = property(lambda x: x._playing)
  277. paused = property(lambda x: x._paused)
  278. dooropen = property(lambda x: x._dooropen)
  279. discknown = property(lambda x: x._discknown)
  280. curdisc = property(lambda x: x._discpos)
  281. alldiscs = property(lambda x: x._alldiscs)
  282. repeateall = property(lambda x: x._repeateall)
  283. repeateone = property(lambda x: x._repeateone)
  284. shuffle = property(lambda x: x._shuffle)
  285. program = property(lambda x: x._program)
  286. cdnum = property(lambda x: x._cdnum)
  287. disccount = property(lambda x: x._disccount)
  288. model = property(lambda x: x._model)
  289. def play(self):
  290. '''Start playing at current possition.'''
  291. return self.runCommand('play', 'play')
  292. def stop(self):
  293. '''Stop playing.'''
  294. return self.runCommand('stop', 'stop')
  295. def pause(self):
  296. '''Pause.'''
  297. return self.runCommand('pause', 'pause')
  298. def togglepause(self):
  299. '''Toggle Paused state. Will trigger the respective paused
  300. or playing on the protocol.'''
  301. l = self.runCommand('togglepause', [ 'play', 'pause', ])
  302. return defer.DeferList(l, fireOnOneCallback=True,
  303. fireOnOneErrback=True)
  304. def nexttrack(self):
  305. '''Next Track.'''
  306. raise NotImplementedError
  307. def previoustrack(self):
  308. '''Previous Track.'''
  309. raise NotImplementedError
  310. def poweron(self):
  311. '''Turn power on.'''
  312. if not self.power:
  313. return self.runCommand('poweron', 'poweron')
  314. return defer.succeed(())
  315. def poweroff(self):
  316. '''Turn power off.'''
  317. if self.power:
  318. return self.runCommand('poweroff', 'poweroff',
  319. ignoreresponses=[ 'stop', ])
  320. return defer.succeed(())
  321. def discinfo(self, disc):
  322. '''Report disc info, will stop playback. Returns Deferred.'''
  323. print 'di:, scheduling loaddisc'
  324. return self.loaddisc(disc) \
  325. .addCallback(lambda x:
  326. self.runCommand('querydiscinfo', 'discinfo', disc))
  327. def trackinfo(self, disc, track):
  328. '''Report track info, will stop playback. Returns Deferred.'''
  329. print 'ti:, scheduling loaddisc'
  330. return self.loaddisc(disc).addCallback(lambda x:
  331. self.runCommand('querytrackinfo', 'track_length', disc,
  332. track))
  333. def playdisc(self, disc, track=1):
  334. '''Start playing disc. Returns Deferred.'''
  335. return self.loaddisc(disc, track).addCallback(lambda x:
  336. self.runCommand('playtrack', 'track_info', disc, track))
  337. def cuedisc(self, disc, track=1, noload=False):
  338. '''Cue disc to track. Returns Deferred.'''
  339. # XXX - should we wait for the pause response?
  340. # XXX - we'll recurse, but that should be fine
  341. if noload:
  342. print 'z'
  343. d = defer.succeed(None)
  344. else:
  345. print 'w'
  346. d = self.loaddisc(disc, track)
  347. # XXX - second cue doesn't return track_info or something
  348. d.addCallback(lambda x: self.stop())
  349. return d.addCallback(lambda x:
  350. self.runCommand('cuetrack', 'track_info', disc, track,
  351. ignoreresponses=[ 'pause', ]))
  352. def loaddisc(self, disc, track=1, load=True):
  353. '''Make sure disc is loaded, cue if necessary. Returns Deferred.'''
  354. if self._discpos != disc and load:
  355. print 'x', `self._discpos`, `disc`
  356. d = self.poweron()
  357. d.addCallback(lambda x: self.setcontinuous())
  358. d.addCallback(lambda x: self.cuedisc(disc, noload=True))
  359. else:
  360. print 'y'
  361. d = defer.succeed(None)
  362. d.addCallback(lambda x: self._discpos == disc)
  363. return d
  364. def discs(self):
  365. '''Return set of discs present.'''
  366. return self._discs
  367. def __contains__(self, k):
  368. '''Check if a disc is present in the changer.'''
  369. return k in self._discs
  370. def __len__(self):
  371. '''Number of discs present in the changer. This is not the
  372. capacitity..'''
  373. return len(self._discs)
  374. def __iter__(self):
  375. '''Iterate over all the discs in the changer.'''
  376. # We use this so that when the discs available changes we
  377. # automaticly update this.
  378. for i in xrange(self.disccount):
  379. if i in self:
  380. yield i
  381. def setmode(self, fun):
  382. print 'sm:', `fun`
  383. if fun():
  384. print 'passed!', `self`
  385. return
  386. d = defer.Deferred()
  387. self.insertresponse('status', d)
  388. d.addCallback(lambda x: self.setmode(fun))
  389. # send IR code
  390. print 'sending ir'
  391. self._proto.sendir('\xeb\x91')
  392. return d
  393. def setcontinuous(self, alldiscs=True):
  394. '''Set continuous playback, if alldiscs is False, one disc.'''
  395. fun = lambda: not self.program and not self.shuffle and \
  396. self.alldiscs == alldiscs
  397. if not fun() and (self.playing or self.paused):
  398. d = self.stop()
  399. else:
  400. d = defer.succeed(None)
  401. return d.addCallback(lambda x: self.setmode(fun))
  402. def setshuffle(self, alldiscs=True):
  403. '''Set shuffle playback, if alldiscs is False, one disc.'''
  404. if self.playing or self.paused:
  405. d = self.stop()
  406. raise NotImplementedError
  407. def settimeout(self, timeout=300):
  408. '''After timeout seconds of no commands being issues, and
  409. the player not playing discs, power off.'''
  410. raise NotImplementedError
  411. # Imported from eslink.py, w/ minor changes
  412. @defer.deferredGenerator
  413. def getcddbid(self, disc):
  414. '''Return CDDB id of disc.'''
  415. r = defer.waitForDeferred(self.discinfo(disc))
  416. yield r
  417. r = r.getResult()
  418. if r[0] != disc:
  419. # if the disc isn't present, update the discs
  420. print 'Disc missing, updating discs'
  421. r = defer.waitForDeferred(self.getdiscs())
  422. yield r
  423. r = r.getResult()
  424. raise NoDisc(disc, 'disc %d not present')
  425. r = r[1:]
  426. def addreducems(i, a):
  427. ni = (i[0] + a[0], i[1] + a[1])
  428. m, s = divmod(ni[1], 60)
  429. i = list(i)
  430. i[0] = ni[0] + m
  431. i[1] = s
  432. return tuple(i)
  433. tracks = []
  434. curtot = (0, 2)
  435. tracklens = []
  436. for i in range(1, r[0] + 1):
  437. tracks.append(curtot)
  438. j = defer.waitForDeferred(self.trackinfo(disc, i))
  439. yield j
  440. j = j.getResult()
  441. assert j[0] == disc
  442. assert j[1] == i
  443. j = j[2:]
  444. tracklens.append(j)
  445. curtot = addreducems(curtot, tracklens[-1])
  446. tracks.append(r[1:])
  447. tracks[-1] = addreducems(tracks[-1], (0, 2))
  448. id = cddb.discid(tracks)
  449. yield id
  450. def nulltrim(buf):
  451. s = buf.find('\x00')
  452. if s == -1:
  453. return buf
  454. return buf[:s]
  455. class SLinkProtocol(ConnectedDatagramProtocol):
  456. '''Interface for an S-Link Port. To be passed into something that
  457. passes messages to/from an S-Link Port.'''
  458. simpcmds = {
  459. 'play': '\x00',
  460. 'stop': '\x01',
  461. 'pause': '\x02',
  462. 'togglepause': '\x03',
  463. 'nexttrack': '\x08',
  464. 'queryplayercap': '\x22',
  465. 'poweron': '\x2e',
  466. 'poweroff': '\x2f',
  467. }
  468. _bysimpcmds = dict((v, k) for k, v in simpcmds.iteritems())
  469. #_bysimpcmds['\x0e'] = 'poweron' # Disc loaded?
  470. simpcmds['querystatus'] = '\x0f'
  471. simpcmds['querydiscinfo'] = '\x44'
  472. simpcmds['querytrackinfo'] = '\x45'
  473. simpcmds['playtrack'] = '\x50'
  474. simpcmds['cuetrack'] = '\x51'
  475. simpcmds['querydeckmodel'] = '\x6a'
  476. simpcmds['querydiscs'] = '\x72'
  477. # Bytes returned by cmd 0x61
  478. decksizes = {
  479. '\x05\x63': 5,
  480. '\xfe\x0b': 200,
  481. '\x64\x6b': 300,
  482. '\x64\x0b': 300,
  483. '\xfe\x6b': 300,
  484. '\xc8\x6b': 400,
  485. }
  486. def discdecode(hidisc, x):
  487. disc = array.array('B', x)[0]
  488. if hidisc:
  489. return disc + 200
  490. elif disc == 0x00:
  491. return 100 # XXX?
  492. elif disc >= 0x9a:
  493. return 100 + disc - 0x9a
  494. else:
  495. return bcdtoint(disc)
  496. def discinfo(hidisc, x, dd=discdecode):
  497. a = array.array('B', x)
  498. return dd(hidisc, x), bcdtoint(a[2]), \
  499. bcdtoint(a[3]), bcdtoint(a[4]), bcdtoint(a[5])
  500. def trackinfo(hidisc, x, dd=discdecode):
  501. a = array.array('B', x)
  502. return dd(hidisc, x), bcdtoint(a[1]), \
  503. bcdtoint(a[2]), bcdtoint(a[3])
  504. responses = {
  505. '\x0e': lambda hidisc, x: ('invalidmode', ()),
  506. '\x14': lambda hidisc, x: ('discinfo', (None, )),
  507. '\x15': lambda hidisc, x: ('trackinfo', (None, )),
  508. '\x45': lambda hidisc, x: ('trackinfo', (x, )),
  509. '\x50': lambda hidisc, x, ti=trackinfo: ('track_info',
  510. ti(hidisc, x)),
  511. '\x52': lambda hidisc, x, dd=discdecode: ('tocread',
  512. (dd(hidisc, x),)),
  513. '\x58': lambda hidisc, x, dd=discdecode: ('nowat',
  514. (dd(hidisc, x),)),
  515. '\x60': lambda hidisc, x, di=discinfo: ('discinfo',
  516. di(hidisc, x)),
  517. '\x61': lambda hidisc, x, y=decksizes: ('decksize',
  518. (y[x],)),
  519. '\x62': lambda hidisc, x, ti=trackinfo: ('track_length',
  520. ti(hidisc, x)),
  521. '\x6a': lambda hidisc, x: ('deckmodel',
  522. (nulltrim(x).decode('ascii'),)),
  523. '\x70': lambda hidisc, x: ('status', (x, )),
  524. '\x72': lambda hidisc, x: ('disc_list0', (x, )),
  525. '\x73': lambda hidisc, x: ('disc_list1', (x, )),
  526. '\x74': lambda hidisc, x: ('disc_list2', (x, )),
  527. '\x75': lambda hidisc, x: ('disc_list3', (x, )),
  528. }
  529. def __init__(self):
  530. self._queue = []
  531. self._cdplayers = {}
  532. self._pendingmsgs = {}
  533. self._started = False
  534. # Should be set by class
  535. def startProtocol(self):
  536. self._started = True
  537. for fun, arg in self._queue:
  538. fun(arg)
  539. self._queue = None
  540. def stopProtocol(self):
  541. self._started = False
  542. def connectCDPlayer(self, num, protocol):
  543. if num < 1 or num > 3:
  544. raise ValueError('invalid CD number: %d' % num)
  545. if not isinstance(protocol, CDPlayerProtocol):
  546. raise ValueError('protocol must be CDPlayerProtocol')
  547. if num in self._cdplayers:
  548. raise ValueError('CD player already attached')
  549. transport = SLinkCDTransport(self, num)
  550. if not self._started:
  551. self._queue.append((transport.makeConnection,
  552. protocol))
  553. else:
  554. transport.makeConnection(protocol)
  555. self._cdplayers[num] = transport
  556. def datagramReceived(self, msg, port):
  557. '''Decode message and dispatch (if possible).'''
  558. mar = array.array('B', msg)
  559. if (mar[0] & 0xf0) != 0x90:
  560. print 'unknown msg:', msg.encode('hex')
  561. if msg in self._pendingmsgs:
  562. self._pendingmsgs[msg].cancel()
  563. del self._pendingmsgs[msg]
  564. return
  565. if (mar[0] & 0x8) != 0x8:
  566. # command from another device, ignore
  567. #print 'unkn command:', msg.encode('hex')
  568. return
  569. # From here all msgs are responses
  570. cdnum = (mar[0] & 0x3) + 1
  571. hidisc = False
  572. if cdnum > 3:
  573. hidisc = True
  574. cdnum -= 3
  575. if cdnum not in self._cdplayers:
  576. # We don't have this CD Player registered.
  577. print 'unkn responses %d:' % cdnum, msg.encode('hex')
  578. return
  579. if len(msg) == 2 and msg[1] in self._bysimpcmds:
  580. attr = self._bysimpcmds[msg[1]]
  581. args = ()
  582. elif msg[1] in self.responses:
  583. attr, args = self.responses[msg[1]](hidisc, msg[2:])
  584. else:
  585. print 'foo:', msg.encode('hex')
  586. return
  587. try:
  588. self._cdplayers[cdnum].gotResponse(attr, *args)
  589. except TypeError, e:
  590. if str(e) == 'gotResponse() argument after * must be a sequence':
  591. raise TypeError('args for %s not sequence' % `msg[1].encode('hex')`)
  592. else:
  593. print 'te:', `str(e)`
  594. raise
  595. # Imported from eslink.py
  596. def makeaddrcmd(self, msgto, cmd, cdnum, disc=None, track=None,
  597. hibit=False):
  598. addr = 0x90 + (cdnum - 1)
  599. if not msgto:
  600. addr |= 0x8
  601. if disc != None:
  602. if disc > 200:
  603. addr += 3
  604. disc -= 200
  605. elif disc < 100:
  606. disc = inttobcd(disc)
  607. else:
  608. disc = 0x9a + disc - 100
  609. if track is None:
  610. return '%c%c%c' % (addr, cmd, disc)
  611. else:
  612. track = inttobcd(track)
  613. return '%c%c%c%c' % (addr, cmd, disc, track)
  614. else:
  615. if hibit:
  616. addr += 3
  617. return '%c%c' % (addr, cmd)
  618. def schedulemsg(self, msg):
  619. print 'sending msg:', `msg`
  620. self.transport.write(msg)
  621. # XXX - should probably recall sendmsg to reschedule
  622. if msg in self._pendingmsgs:
  623. print 'old:', self._pendingmsgs[msg]
  624. self._pendingmsgs[msg] = reactor.callLater(1,
  625. lambda m=msg: self.schedulemsg(msg))
  626. def sendir(self, code):
  627. # XXX make it properly addressable
  628. self.transport.write(code)
  629. def sendmsg(self, type, cdpnum, disc=None, track=None):
  630. msg = self.makeaddrcmd(True, self.simpcmds[type], cdpnum,
  631. disc, track)
  632. self.schedulemsg(msg)
  633. def bcdtoints(i):
  634. t, o = divmod (i, 16)
  635. assert t < 10 and o < 10
  636. return t, o
  637. def inttobcd(i):
  638. assert i < 100, 'i is invalid: %s' % `i`
  639. t, o = divmod(i, 10)
  640. return t << 4 | o
  641. def bcdtoint(i):
  642. t, o = divmod (i, 16)
  643. assert t < 10 and o < 10
  644. return t * 10 + o
  645. class SLinkEPort(object):
  646. '''Implement one port transport for SLink-E.'''
  647. def __init__(self, slinke, port, protocol):
  648. self.slinke = slinke
  649. self.port = port
  650. self.protocol = protocol
  651. self.openned = True
  652. def write(self, msg):
  653. if not self.openned:
  654. raise RuntimeError('port has been closed')
  655. self.slinke.sendmsg(self.port, [ msg ])
  656. def writeSequence(self, data):
  657. self.slinke.sendmsg(self.port, data)
  658. def loseConnection(self):
  659. '''Close the port. Will return a Deferred object that is
  660. called when the port has finished closing.'''
  661. self.openned = False
  662. self.slinke.closePort(self.port)
  663. def getPeer(self):
  664. return self.port
  665. class SLinkE(Protocol):
  666. '''Interface for talking to the SLink-E. It sends commands, and
  667. dispatched commands recevied for the port.'''
  668. commands = {
  669. 'reset': '\xff\xff',
  670. 'getversion': '\xff\x0b',
  671. 'getserial': '\xff\x0c',
  672. 'disableport': lambda x: chr((x << 5) | 0x1f) + '\x02',
  673. 'enableport': lambda x: chr((x << 5) | 0x1f) + '\x03',
  674. }
  675. # XXX - It's assumed all responses are two bytes.
  676. receivedresp = {
  677. 'versionis': ('\xff\x0b', 1),
  678. 'portenabled': (lambda x: (ord(x[0]) & 0x1f) == 0x1f and
  679. x[1] == '\x03', 0),
  680. 'portdisabled': (lambda x: (ord(x[0]) & 0x1f) == 0x1f and
  681. x[1] == '\x02', 0),
  682. 'slinkerror': (lambda x: (ord(x[0]) & 0x1f) == 0x1f and
  683. x[1] == '\x80', 0),
  684. }
  685. version = property(lambda x: x._version)
  686. #serial = property(lambda x: x._serial)
  687. def __init__(self, *args, **kwargs):
  688. self._ports = {}
  689. self._portqueue = {}
  690. self._portpending = {}
  691. self._buffer = []
  692. self._bufferlen = 0
  693. self._currentresp = None
  694. self._bytesneeded = None
  695. _version = (None, None)
  696. def sendcmd(self, cmd):
  697. #print 'sending:', `cmd`
  698. self.transport.write(cmd)
  699. def openPort(self, port, protocol):
  700. if port in self._ports:
  701. raise ValueError('port already opened')
  702. if port in self._portpending:
  703. raise ValueError('port already pending')
  704. self.sendcmd(self.commands['enableport'](port))
  705. self._portpending[port] = SLinkEPort(self, port, protocol)
  706. def closePort(self, port):
  707. self.sendcmd(self.commands['disableport'](port))
  708. def sendmsg(self, port, msg):
  709. '''Send the sequence of strings in msg out the port.'''
  710. pos = 0
  711. fragpos = 0
  712. msgbuf = []
  713. while pos != len(msg) and fragpos != len(msg[-1]):
  714. fraglen = 0
  715. msgfrag = []
  716. while fraglen < 30 and pos != len(msg) and \
  717. fragpos != len(msg[-1]):
  718. msgfrag.append(msg[pos][fragpos:fragpos + (30 - fraglen)])
  719. fragpos += len(msgfrag[-1])
  720. fraglen += len(msgfrag[-1])
  721. if fragpos == len(msg[pos]):
  722. pos += 1
  723. fragpos = 0
  724. #print 'h:', `fraglen`, `msgfrag`
  725. msgbuf.append(chr((port << 5) | fraglen))
  726. msgbuf.extend(msgfrag)
  727. # Port Send End
  728. msgbuf.append(chr(port << 5))
  729. #print 'sending:', `msgbuf`
  730. self.transport.writeSequence(msgbuf)
  731. # Protocol Methods
  732. def connectionMade(self):
  733. Protocol.connectionMade(self)
  734. #self.sendcmd(self.commands['reset'])
  735. #time.sleep(1)
  736. self.sendcmd(self.commands['getversion'])
  737. #self.sendcmd(self.commands['disableport'](7))
  738. #for i in xrange(6):
  739. # self.sendcmd(self.commands['enableport'](i))
  740. def dataReceived(self, data):
  741. #print 'dR:', `data`
  742. self._buffer.append(data)
  743. self._bufferlen += len(data)
  744. i = True
  745. while i and self._buffer:
  746. i = self.checkBuffer()
  747. # Internal Buffer routines
  748. def peakBuffer(self, cnt):
  749. if cnt > self._bufferlen:
  750. return
  751. r = []
  752. rem = cnt
  753. for buf in self._buffer:
  754. r.append(buf[:rem])
  755. rem -= len(r[-1])
  756. if rem == 0:
  757. break
  758. r = ''.join(r)
  759. assert len(r) == cnt, 'failed: cnt=%d, buffer=%s' % (cnt, self._buffer)
  760. return r
  761. def consumeBuffer(self, cnt):
  762. assert self._bufferlen >= cnt
  763. rem = cnt
  764. while rem:
  765. buf = self._buffer.pop(0)
  766. self._bufferlen -= len(buf)
  767. if len(buf) > rem:
  768. self._buffer.insert(0, buf[rem:])
  769. self._bufferlen += len(buf) - rem
  770. return
  771. rem -= len(buf)
  772. def getBuffer(self, cnt):
  773. buf = self.peakBuffer(cnt)
  774. if buf is None:
  775. return
  776. self.consumeBuffer(cnt)
  777. return buf
  778. def lenBuffer(self):
  779. return self._bufferlen
  780. def checkBuffer(self):
  781. '''Dispatch messages if available in buffer. Returns True if
  782. a message was dispatched. Returns False if we need more data
  783. to dispatch.'''
  784. if self._currentresp is not None:
  785. raise NotImplementedError
  786. firstbyte = ord(self._buffer[0][0])
  787. portnum = firstbyte >> 5
  788. bytecnt = (firstbyte & 0x1f)
  789. #print 'portnum:', portnum, 'bytecnt:', bytecnt, 'buf:', self._buffer
  790. if bytecnt == 0:
  791. # port receive end
  792. self.consumeBuffer(1)
  793. if portnum not in self._portqueue:
  794. # XXX - stale data
  795. return
  796. msg = ''.join(self._portqueue[portnum])
  797. self._portqueue[portnum] = []
  798. #print 'delivering msg %d:' % portnum, `msg`
  799. self._ports[portnum].protocol.datagramReceived(msg, portnum)
  800. elif bytecnt == 0x1f:
  801. # modify command
  802. buf = self.peakBuffer(2)
  803. if buf is None:
  804. return False
  805. for k, (resp, extracnt) in self.receivedresp.iteritems():
  806. if (callable(resp) and resp(buf)) or buf == resp:
  807. if self.lenBuffer() < len(buf) + extracnt:
  808. return False
  809. self.consumeBuffer(len(buf))
  810. extra = self.getBuffer(extracnt)
  811. self.dispatch(k, portnum, extra)
  812. return True
  813. else:
  814. raise RuntimeError('unhandled msg: %s' % `''.join(self._buffer)`)
  815. else:
  816. # port receive data
  817. buf = self.peakBuffer(bytecnt + 1)
  818. if buf is None:
  819. return False
  820. #print 'f:', bytecnt, `buf`
  821. self.consumeBuffer(bytecnt + 1)
  822. if portnum in self._portqueue:
  823. self._portqueue[portnum].append(buf[1:])
  824. return True
  825. def dispatch(self, k, portnum, extra):
  826. '''Dispatch a message.'''
  827. #print 'dispatching: %s(%d, %s)' % (`k`, portnum, `extra`)
  828. getattr(self, 'R_%s' % k)(portnum, extra)
  829. # Messages
  830. def R_versionis(self, portnum, data):
  831. assert len(data) == 1
  832. self._version = bcdtoints(ord(data[0]))
  833. #print 'version:', `self._version`
  834. def R_portdisabled(self, portnum, data):
  835. if portnum == 7:
  836. assert not self._ports, \
  837. 'disabled, but ports present: %s' % `self._ports`
  838. return
  839. pobj = self._ports[portnum]
  840. del self._ports[portnum]
  841. del self._portqueue[portnum]
  842. # Signal that we are done
  843. pobj.protocol.doStop()
  844. def R_portenabled(self, portnum, data):
  845. if portnum not in self._portpending:
  846. #print 'skipping:', portnum
  847. return
  848. self._ports[portnum] = self._portpending[portnum]
  849. self._portqueue[portnum] = []
  850. del self._portpending[portnum]
  851. self._ports[portnum].protocol.makeConnection(self._ports[portnum])
  852. def R_slinkerror(self, portnum, data):
  853. pass
  854. #print 'got slinkerror on port:', `portnum`
  855. # Test Code
  856. class MyCDPlayer(CDPlayerProtocol):
  857. @staticmethod
  858. def printarg(args):
  859. print 'pa:', `args`
  860. def connectionMade(self):
  861. print 'power:', `self.transport.power`
  862. if not self.transport.power:
  863. print 'pon:', `self.transport.poweron()`
  864. print self.transport.discs()
  865. self.transport.trackinfo(32, 5).addCallback(
  866. self.printarg).addCallback(lambda x: self.transport.discinfo(32)).addCallback(self.printarg).addCallback(lambda x: self.transport.poweroff()).addCallback(lambda x: reactor.stop())
  867. #reactor.callLater(20, self.transport.poweroff)
  868. #print 'powered on, playing...'
  869. #self.transport.querydeckmodel()
  870. #self.transport.querystatus()
  871. #self.transport.querydiscinfo(5)
  872. #self.transport.querytrackinfo(5, 3)
  873. #self.transport.playtrack(10, 3)
  874. #print 'powered off...'
  875. #reactor.stop()
  876. def stateChanged(self):
  877. print 'sC'
  878. if __name__ == '__main__':
  879. if False:
  880. import serial
  881. s = serial.Serial('/dev/ttyU0', baudrate=38400, rtscts=True, timeout=5)
  882. s.write('\xff\xff')
  883. print 'reset'
  884. time.sleep(1)
  885. s.write(''.join(['\x02', '\x90"', '\x00']))
  886. print 'cmd'
  887. while True:
  888. print 'resp:', `s.read(1)`
  889. sys.exit(0)
  890. slinke = SLinkE()
  891. s = SerialPort(slinke, '/dev/ttyU0', reactor, baudrate=38400, rtscts=True)
  892. th = SLinkProtocol()
  893. slinke.openPort(0, th)
  894. th.connectCDPlayer(1, MyCDPlayer())
  895. #reactor.callLater(5, lambda: slinke.closePort(0))
  896. reactor.run()