superdac.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Requires:
  5. # apt install mpd
  6. # apt install mpc
  7. # apt install lirc
  8. # apt install python3-pip
  9. # apt install python3-rpi.gpio
  10. # apt install python3-pil
  11. # apt install python3-numpy
  12. # apt install python3-lirc
  13. # apt install fonts-takao
  14. # apt install exfat-fuse exfat-utils
  15. # pip3 install pyalsaaudio
  16. # pip3 install pysmb
  17. # Python_ST7735-raspimag.tar.gz
  18. # python-mpd2.tar.gz
  19. # stdlibs
  20. import sys
  21. import os
  22. import signal
  23. import threading
  24. from threading import BoundedSemaphore
  25. from threading import Semaphore
  26. import queue
  27. import platform
  28. from select import select
  29. from time import sleep
  30. from urllib.parse import urlparse
  31. import subprocess
  32. import hashlib
  33. import glob
  34. # ABCMeta
  35. from abc import ABCMeta, abstractmethod
  36. # Raspberry pi
  37. import RPi.GPIO as GPIO
  38. # PIL
  39. from PIL import Image
  40. from PIL import ImageDraw
  41. from PIL import ImageFont
  42. # Adafruit
  43. import ST7735 as TFT
  44. # import Adafruit_GPIO as GPIO
  45. import Adafruit_GPIO.SPI as SPI
  46. # lirc
  47. import lirc
  48. # mpd
  49. from mpd import MPDClient
  50. from socket import error as SocketError
  51. # alsaaudio
  52. from alsaaudio import Mixer
  53. # SMB
  54. from smb.SMBConnection import SMBConnection
  55. from nmb.NetBIOS import NetBIOS
  56. # sdlib
  57. from sdlib import simpleSMB
  58. from sdlib import castQueue
  59. from sdlib import SDTimer
  60. # LCD spec.
  61. LCD_WIDTH = 128
  62. LCD_HEIGHT = 160
  63. SPEED_HZ = 8000000
  64. # SUPERDAC configration
  65. DC = 24
  66. RST = 23
  67. SPI_PORT = 0
  68. SPI_DEVICE = 0
  69. SW1 = 5
  70. SW2 = 6
  71. # FONT settings
  72. DEFAULT_FONT = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
  73. FONT_SIZE = 12
  74. # PID File
  75. PIDFILE='/var/run/superdac.pid'
  76. LOGFILE='/var/log/superdac.log'
  77. # MPD Socket
  78. MPD_HOST='/var/run/mpd/socket'
  79. MPD_PORT=6600
  80. # lirc configration files
  81. LIRCRC = ['/etc/lirc/superdac.lircrc', '/usr/local/etc/superdac.lircrc', './superdac.lircrc' ]
  82. # Volume file
  83. VOLUME_FILE='/var/tmp/.superdac.volume'
  84. # Openning Logo Image
  85. LOGO_FILE = '/home/mpd/scripts/logo.jpg'
  86. # Message class
  87. class Message():
  88. TERM = 0 # 'term'
  89. DISPLAY = 1 # 'disp'
  90. RCONTROL = 2 # 'rcontrol'
  91. MENUON = 3 # 'menuon'
  92. MENUOFF = 4 # 'menuoff'
  93. MPD = 5 # 'mpd'
  94. MPC = 6 # 'mpc'
  95. PLAYLIST = 7 # 'playlist'
  96. INDICATOR = 8 # 'indicate'
  97. MPC_REPLY = 9 # 'mpc_reply'
  98. VOLUME = 10 # 'volume'
  99. SYSMENU = 11 # 'sysmenu'
  100. def __init__(self, sender, message, param):
  101. self.__sender = sender
  102. self.__message = message
  103. self.__param = param
  104. def getSender(self):
  105. return self.__sender
  106. def getMessage(self):
  107. return self.__message
  108. def getParam(self):
  109. return self.__param
  110. # LCD Display Item
  111. class LCDItem():
  112. FULL = 0
  113. TOP = 1
  114. INDICATE = 2
  115. BOTTOM = 3
  116. def __init__(self, image, clear=False, pos=FULL, time=0):
  117. self.__image = image
  118. self.__pos = pos
  119. self.__time = time
  120. self.__clear = clear
  121. return
  122. def getClear(self):
  123. return self.__clear
  124. def getImage(self):
  125. return self.__image
  126. def getPos(self):
  127. return self.__pos
  128. def getTime(self):
  129. return self.__time
  130. def getWidth(self):
  131. return self.__image.size[0]
  132. def getHeight(self):
  133. return self.__image.size[1]
  134. # Worker Base
  135. class Worker(metaclass=ABCMeta):
  136. # type: (Object, Master)
  137. def __init__(self, pm):
  138. self.playermain = pm
  139. def sendMessage(self, message):
  140. self.playermain.postMessage(message)
  141. @abstractmethod
  142. def receiveMessage(self, message):
  143. pass
  144. # Master Base
  145. class Master(metaclass=ABCMeta):
  146. @abstractmethod
  147. def postMessage(self, message):
  148. pass
  149. # Switch worker
  150. class Switches(Worker):
  151. def __init__(self, pm):
  152. # type: (Object, Master)
  153. super().__init__(pm)
  154. GPIO.setmode(GPIO.BCM)
  155. GPIO.setup(SW1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
  156. GPIO.setup(SW2, GPIO.IN, pull_up_down=GPIO.PUD_UP)
  157. GPIO.add_event_detect(SW1, GPIO.FALLING, callback=self.swCallback, bouncetime=20)
  158. GPIO.add_event_detect(SW2, GPIO.FALLING, callback=self.swCallback, bouncetime=20)
  159. def swCallback(self, ch):
  160. if ch == SW1:
  161. os.system('/sbin/reboot')
  162. elif ch == SW2:
  163. os.system('/sbin/poweroff')
  164. return
  165. def receiveMessage(self, message):
  166. if message.getMessage() == Message.TERM:
  167. GPIO.remove_event_detect(SW1)
  168. GPIO.remove_event_detect(SW2)
  169. GPIO.cleanup(SW1)
  170. GPIO.cleanup(SW2)
  171. return True
  172. # Playlist manage worker
  173. class Playlist(Worker):
  174. active = False
  175. playermain = None
  176. playlists = []
  177. artists = []
  178. albums = []
  179. items = []
  180. current = []
  181. type = ''
  182. LINES = 10
  183. LINE_HEIGHT = 16
  184. def __init__(self, pm):
  185. # type: (Object, Master)
  186. super().__init__(pm)
  187. # 初期データ取得
  188. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
  189. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
  190. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
  191. def makeMenu(self, draw, font):
  192. self.current = self.items[0:self.LINES-1]
  193. del self.items[0:self.LINES-1]
  194. for i in range(0, len(self.current)):
  195. draw.text((0, i*self.LINE_HEIGHT), str(i+1)+':'+self.current[i], font=font, fill='#FFFFFF' )
  196. i = i + 1
  197. if len(self.items) == 0:
  198. draw.text((0,i*self.LINE_HEIGHT), '0:中止', font=font, fill='#FFFFFF' )
  199. else:
  200. draw.text((0,i*self.LINE_HEIGHT), '0:次へ', font=font, fill='#FFFFFF' )
  201. return
  202. def receiveMessage(self, message):
  203. # リスト取得
  204. if message.getMessage() == Message.MPC_REPLY:
  205. if message.getParam()['type'] == 'playlists':
  206. self.playlists = message.getParam()['list']
  207. elif message.getParam()['type'] == 'artist':
  208. self.artists = message.getParam()['list']
  209. elif message.getParam()['type'] == 'album':
  210. self.albums = message.getParam()['list']
  211. return True
  212. if message.getMessage() == Message.PLAYLIST:
  213. if not self.active:
  214. self.type = message.getParam()
  215. if self.type == 'playlists':
  216. self.items = self.playlists
  217. elif self.type == 'artist':
  218. self.items = self.artists
  219. elif self.type == 'album':
  220. self.items = self.albums
  221. image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
  222. font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
  223. draw = ImageDraw.Draw(image)
  224. # プレイリストが空
  225. if len(self.items) == 0:
  226. draw.text((0,64), 'プレイリストが', font=font, fill='#FF0000')
  227. draw.text((0,80), ' ありません', font=font, fill='#FF0000')
  228. item = LCDItem(image, time=2)
  229. self.sendMessage(Message(self, Message.DISPLAY, item))
  230. self.sendMessage(Message(self, Message.MENUOFF, None ))
  231. return False
  232. else:
  233. # 新規メニュー描画
  234. self.makeMenu(draw, font)
  235. item = LCDItem(image)
  236. self.playermain.lcd.displayItem(item, lock=True)
  237. self.active = True
  238. return False
  239. return False
  240. # リモコン
  241. if message.getMessage() == Message.RCONTROL and self.active:
  242. # 数字ボタン
  243. if message.getParam() in '0123456789':
  244. btn = int(message.getParam()) - 1
  245. if btn < 0:
  246. if len(self.items) == 0:
  247. # 終了
  248. self.active = False
  249. self.sendMessage(Message(self, Message.MENUOFF, None ))
  250. self.playermain.lcd.release()
  251. else:
  252. # 次ページ
  253. image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
  254. font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
  255. draw = ImageDraw.Draw(image)
  256. self.makeMenu(draw, font)
  257. item = LCDItem(image)
  258. self.playermain.lcd.release()
  259. self.playermain.lcd.displayItem(item, lock=True)
  260. self.active = True
  261. return False
  262. elif btn < len(self.current):
  263. if self.type == 'playlists':
  264. self.sendMessage(Message(self, Message.MPC, {'command': 'load', 'arg': self.current[btn]}))
  265. else:
  266. param = {'command':'findadd', 'arg': {'type':self.type,'value': self.current[btn]}}
  267. msg = Message(self, Message.MPC, param)
  268. self.sendMessage(msg)
  269. self.playermain.lcd.release()
  270. self.sendMessage(Message(self, Message.MENUOFF, None ))
  271. self.active = False
  272. return True
  273. return True
  274. # メインメニューに戻る
  275. elif message.getParam() == 'menu':
  276. self.playermain.lcd.release()
  277. self.active = False
  278. return True
  279. return True
  280. # system menu
  281. class SysMenu(Worker):
  282. active = False
  283. def __init__(self, pm):
  284. super().__init__(pm)
  285. def receiveMessage(self, message):
  286. if message.getMessage() == Message.SYSMENU:
  287. if self.active == False:
  288. self.active = True
  289. image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
  290. jpfont = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
  291. draw = ImageDraw.Draw(image)
  292. draw.text((0,0) , '1:再起動' , font=jpfont, fill='#FFFFFF')
  293. draw.text((0,16) , '2:シャットダウン', font=jpfont, fill='#FFFFFF')
  294. draw.text((0,32) , '3:キャッシュ消去', font=jpfont, fill='#FFFFFF')
  295. draw.text((0,144), '0:中止' , font=jpfont, fill='#FFFFFF')
  296. item = LCDItem(image)
  297. self.playermain.lcd.displayItem(item, lock=True)
  298. return False
  299. elif message.getMessage() == Message.RCONTROL and self.active:
  300. c = message.getParam()
  301. if c in '0123':
  302. if c == '1': # reboot
  303. os.system('/sbin/reboot')
  304. self.active = False
  305. self.sendMessage(Message(self, Message.MENUOFF, None ))
  306. elif c == '2': # shutdown
  307. os.system('/sbin/poweroff')
  308. self.active = False
  309. self.sendMessage(Message(self, Message.MENUOFF, None ))
  310. elif c == '3': # cache clear
  311. cache_dir = AlbumArt.TEMP_DIR
  312. [os.remove(f) for f in glob.glob(cache_dir+'/*' )]
  313. self.active = False
  314. self.sendMessage(Message(self, Message.MENUOFF, None ))
  315. elif c == '0': # cacnel
  316. self.active = False
  317. self.sendMessage(Message(self, Message.MENUOFF, None ))
  318. elif c == 'menu': # メインメニューに戻る
  319. self.active = False
  320. self.playermain.lcd.release()
  321. return True
  322. if self.active == False:
  323. self.playermain.lcd.release()
  324. return False
  325. return True
  326. return True
  327. # MainMenu Worker
  328. class MainMenu(Worker):
  329. active = False
  330. playermain = None # type: Master
  331. status = []
  332. playlists = []
  333. def __init__(self, pm):
  334. # type: (Object, Master)
  335. super().__init__(pm)
  336. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
  337. def receiveMessage(self,message):
  338. # MPD Message
  339. if message.getMessage() == Message.MPD:
  340. param = message.getParam()
  341. self.status = param['status']
  342. return True
  343. if message.getMessage() == Message.MPC_REPLY:
  344. if message.getParam()['type'] == 'playlists':
  345. self.playlists = message.getParam()['list']
  346. return True
  347. # リモコン以外は無視
  348. if message.getMessage() != Message.RCONTROL:
  349. return True
  350. # Remote Control Message
  351. if message.getParam() == 'menu':
  352. # おそらくありえない
  353. if len(self.status) == 0:
  354. return True
  355. # アクティブなら閉じる
  356. if self.active:
  357. self.active = False
  358. self.playermain.lcd.release()
  359. self.sendMessage(Message(self, Message.MENUOFF, None ))
  360. return True
  361. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists'} ))
  362. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
  363. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
  364. # メニュー描画
  365. image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
  366. font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
  367. draw = ImageDraw.Draw(image)
  368. if self.status['random'] == '1':
  369. draw.text((0,0),'1:ノーマル再生', font=font,fill='#FFFFFF')
  370. else:
  371. draw.text((0,0),'1:ランダム再生', font=font,fill='#FFFFFF')
  372. if self.status['repeat'] == '1':
  373. draw.text((0,16),'2:リピートしない', font=font,fill='#FFFFFF')
  374. else:
  375. draw.text((0,16),'2:リピート再生', font=font,fill='#FFFFFF')
  376. draw.text((0,32 ), '3:現在の曲を外す', font=font,fill='#FFFFFF')
  377. draw.text((0,48 ), '4:プレイリスト保存', font=font,fill='#FFFFFF')
  378. draw.text((0,64 ), '5:全曲再生', font=font,fill='#FFFFFF')
  379. draw.text((0,80 ), '6:プレイリスト読込', font=font,fill='#FFFFFF')
  380. draw.text((0,96 ), '7:アルバム選択', font=font,fill='#FFFFFF')
  381. draw.text((0,112), '8:アーティスト選択', font=font,fill='#FFFFFF')
  382. draw.text((0,128), '9:DBアップデート', font=font,fill='#FFFFFF')
  383. draw.text((0,144), '0:システムメニュー', font=font,fill='#FFFFFF')
  384. item = LCDItem(image, pos=LCDItem.FULL)
  385. self.playermain.lcd.displayItem(item,lock=True)
  386. self.active = True
  387. return True
  388. # アクティブかつリモコンが押された
  389. if self.active:
  390. c = message.getParam()
  391. if c in '0123456789':
  392. if c == '1': # random
  393. arg = 'on'
  394. if self.status['random'] == '1':
  395. arg = 'off'
  396. self.sendMessage(Message(self, Message.MPC, {'command':'random', 'arg': arg} ))
  397. self.active = False
  398. self.sendMessage(Message(self, Message.MENUOFF, None ))
  399. elif c == '2': # repeat
  400. arg = 'on'
  401. if self.status['repeat'] == '1':
  402. arg = 'off'
  403. self.sendMessage(Message(self, Message.MPC, {'command':'repeat', 'arg': arg} ))
  404. self.active = False
  405. self.sendMessage(Message(self, Message.MENUOFF, None ))
  406. elif c == '3': # delete
  407. self.sendMessage(Message(self, Message.MPC, {'command':'delete', 'arg': None} ))
  408. self.active = False
  409. self.sendMessage(Message(self, Message.MENUOFF, None ))
  410. elif c == '4': # save
  411. for i in range(0,100):
  412. if not (('playlist'+str(i)) in self.playlists):
  413. break
  414. self.sendMessage(Message(self, Message.MPC, {'command':'save', 'arg': 'playlist'+str(i)}))
  415. self.active = False
  416. self.sendMessage(Message(self, Message.MENUOFF, None ))
  417. elif c == '5': # play all
  418. self.sendMessage(Message(self, Message.MPC, {'command':'playall', 'arg': None} ))
  419. self.active = False
  420. self.sendMessage(Message(self, Message.MENUOFF, None ))
  421. elif c == '6': # playlist
  422. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
  423. self.sendMessage(Message(self, Message.PLAYLIST, 'playlists' ))
  424. self.active = False
  425. elif c == '7': # album
  426. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
  427. self.sendMessage(Message(self, Message.PLAYLIST, 'album' ))
  428. self.active = False
  429. elif c == '8': # artist
  430. self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
  431. self.sendMessage(Message(self, Message.PLAYLIST, 'artist' ))
  432. self.active = False
  433. elif c == '9': # db update
  434. self.sendMessage(Message(self, Message.MPC, {'command':'update', 'arg': None} ))
  435. self.active = False
  436. self.sendMessage(Message(self, Message.MENUOFF, None ))
  437. elif c == '0': # sysmenu
  438. self.sendMessage(Message(self, Message.SYSMENU, None))
  439. self.active = False
  440. if self.active == False:
  441. self.playermain.lcd.release()
  442. return False
  443. return True
  444. return True
  445. # Volume Control Worker
  446. class VolumeControl(Worker):
  447. MAX_VALUE = 81 # 81% == 0dB
  448. enabled = True
  449. term = False
  450. value = 100
  451. mute = False
  452. def __init__(self, pm):
  453. super().__init__(pm)
  454. # read initial value
  455. if os.path.exists(VOLUME_FILE):
  456. with open(VOLUME_FILE, "r") as f:
  457. try:
  458. self.value = int(f.read())
  459. except Exception as e:
  460. pass
  461. try:
  462. self.mixer = Mixer('Digital')
  463. except Exception as e:
  464. print(e, file=self.playermain.out)
  465. self.enabled = False
  466. if self.enabled:
  467. self.mixer.setvolume(int((self.MAX_VALUE *self.value)/100))
  468. self.mixer.setmute(self.mute)
  469. def receiveMessage(self,message):
  470. if message.getMessage() == Message.RCONTROL:
  471. # enabled ?
  472. if not self.enabled:
  473. return True
  474. if message.getParam() == 'volup':
  475. self.value = self.value + 2
  476. if self.value > 100:
  477. self.value = 100
  478. elif message.getParam() == 'voldown':
  479. self.value = self.value - 2
  480. if self.value < 0:
  481. self.value = 0
  482. elif message.getParam() == 'mute':
  483. self.mute = not(self.mute)
  484. self.mixer.setmute(self.mute)
  485. self.sendMessage(Message(self, Message.VOLUME, {'type':'mute', 'value': self.mute}))
  486. if message.getParam() == 'volup' or message.getParam() == 'voldown':
  487. self.mixer.setvolume(int((self.MAX_VALUE*self.value)/100))
  488. self.sendMessage(Message(self, Message.VOLUME, {'type':'volume', 'value': self.value}))
  489. # Terminate
  490. elif message.getMessage() == Message.TERM:
  491. try:
  492. with open(VOLUME_FILE, "w") as f:
  493. f.write(str(self.value))
  494. except Exception as e:
  495. pass
  496. return True
  497. # LCD Worker
  498. class LCD(Worker, threading.Thread):
  499. term = False
  500. HEIGHT = LCD_HEIGHT
  501. WIDTH = LCD_WIDTH
  502. BOTTOM_HEIGHT = 12
  503. INDICATE_HEIGHT = 10
  504. TOP_HEIGHT = 138
  505. term = False
  506. def __init__(self, pm):
  507. threading.Thread.__init__(self)
  508. Worker.__init__(self,pm)
  509. self.spi = SPI.SpiDev(SPI_PORT, SPI_DEVICE,max_speed_hz=SPEED_HZ)
  510. self.disp = TFT.ST7735(DC, rst=RST, spi=self.spi)
  511. self.disp.begin()
  512. self.disp.clear((0,0,0));
  513. if os.path.exists(LOGO_FILE):
  514. image = Image.open(LOGO_FILE)
  515. self.disp.buffer.paste(image)
  516. self.disp.display()
  517. self.messageq = castQueue(5)
  518. self.semaphore = BoundedSemaphore()
  519. def receiveMessage(self, message):
  520. if message.getMessage() == Message.DISPLAY:
  521. self.messageq.put(message.getParam())
  522. return False
  523. elif message.getMessage() == Message.TERM:
  524. self.term = True
  525. return True
  526. def release(self):
  527. try:
  528. self.semaphore.release()
  529. except Exception as e:
  530. print(e, file=self.playermain.out)
  531. def displayItem(self, item, lock=False):
  532. self.semaphore.acquire()
  533. try:
  534. # 画面全体に貼り付け
  535. if item.getPos() == LCDItem.FULL:
  536. if item.getClear():
  537. self.disp.clear((0,0,0))
  538. if item.getImage() is not None:
  539. self.disp.buffer.paste(item.getImage())
  540. self.disp.display()
  541. else:
  542. left = 0
  543. upper = 0
  544. item_width = item.getWidth()
  545. item_height = item.getHeight()
  546. image = item.getImage()
  547. # サイズ制限
  548. if item.getWidth() != self.WIDTH:
  549. scale = float(float(self.WIDTH)/float(item.getWidth()))
  550. image = item.getImage().resize((int(item_width * scale), int(item_height*scale)), Image.BILINEAR)
  551. # Display TOP
  552. if item.getPos() == LCDItem.TOP:
  553. topimg = Image.new('RGB', (LCD.WIDTH, LCD.TOP_HEIGHT), 0)
  554. topimg.paste(image)
  555. self.disp.display(topimg, x0=left, y0=upper, x1=self.WIDTH-1, y1=topimg.size[1]-1 )
  556. # Display Bottom
  557. elif item.getPos() == LCDItem.BOTTOM:
  558. if image.size[1] > self.BOTTOM_HEIGHT:
  559. image = image.crop( box=(0, 0, self.WIDTH, self.BOTTOM_HEIGHT) )
  560. self.disp.display(image, x0=left, y0 = self.HEIGHT-self.BOTTOM_HEIGHT-1, x1=self.WIDTH-1, y1=self.HEIGHT-1 )
  561. # Display Indicator
  562. elif item.getPos() == LCDItem.INDICATE:
  563. if image.size[1] > self.INDICATE_HEIGHT:
  564. image = image.crop( box=(0, 0, self.WIDTH, self.INDICATE_HEIGHT) )
  565. self.disp.display(image, x0=left, y0 = self.HEIGHT-self.BOTTOM_HEIGHT-self.INDICATE_HEIGHT-1, x1=self.WIDTH-1, y1=self.HEIGHT-self.BOTTOM_HEIGHT-1 )
  566. if item.getTime() != 0:
  567. sleep(item.getTime())
  568. except Exception as e: # 何かエラーが起きたら無条件で開放
  569. print(e, file=self.playermain.out)
  570. self.release()
  571. return
  572. if lock == False:
  573. self.release()
  574. return
  575. def run(self):
  576. while self.term == False:
  577. try:
  578. # LCDItem
  579. item = self.messageq.get_nowait()
  580. self.displayItem(item)
  581. except queue.Empty:
  582. sleep(0.05)
  583. return True
  584. # MPD state watch worker
  585. class MPD(Worker, threading.Thread):
  586. term = False
  587. def __init__(self, pm):
  588. threading.Thread.__init__(self)
  589. Worker.__init__(self,pm)
  590. try:
  591. self.mpc = MPDClient(use_unicode=True)
  592. self.mpc.connect( MPD_HOST, MPD_PORT );
  593. except SocketError as e:
  594. print(e, file=self.playermain.out);
  595. return
  596. self.config = self.mpc.config()
  597. current = self.mpc.currentsong()
  598. status = self.mpc.status()
  599. self.param = {'changes':None, 'current': current, 'status': status, 'config': self.config }
  600. def receiveMessage(self,message):
  601. if message.getMessage() == Message.TERM:
  602. self.term = True
  603. elif message.getMessage() == Message.MENUOFF:
  604. self.sendMessage()
  605. return True
  606. def sendMessage(self):
  607. super().sendMessage(Message(self, Message.MPD, self.param))
  608. return
  609. def run(self):
  610. # 現状レポート
  611. current = self.mpc.currentsong()
  612. status = self.mpc.status()
  613. self.param = {'changes':None, 'current': current, 'status': status, 'config': self.config }
  614. self.sendMessage()
  615. self.mpc.send_idle()
  616. while self.term == False:
  617. canRead = select([self.mpc], [], [], 0)[0]
  618. if canRead:
  619. changes = self.mpc.fetch_idle()
  620. current = self.mpc.currentsong()
  621. status = self.mpc.status()
  622. self.param = {'changes': changes, 'current': current, 'status': status, 'config': self.config }
  623. self.sendMessage()
  624. self.mpc.send_idle()
  625. sleep(0.1)
  626. # idle中にCloseすると怒られる
  627. # self.mpc.close()
  628. return
  629. # Album art worker
  630. class AlbumArt(Worker, threading.Thread):
  631. TEMP_DIR = '/var/tmp/.superdac.aa'
  632. EXTRACTCA = '/usr/local/bin/extractCoverArt'
  633. term = False
  634. refresh = False
  635. prev_path = 'foobar'
  636. term = False
  637. use_folderjpg = True
  638. def __init__(self, pm):
  639. threading.Thread.__init__(self)
  640. Worker.__init__(self,pm)
  641. self.messageq = queue.Queue()
  642. if not os.path.exists(self.TEMP_DIR):
  643. os.mkdir(self.TEMP_DIR)
  644. if os.path.exists(self.EXTRACTCA):
  645. self.use_folderjpg = False
  646. def extractCoverArt(self, config, current):
  647. if 'file' in current:
  648. song_file = config + '/' + current['file']
  649. cache_file = self.TEMP_DIR + '/' + hashlib.sha256(song_file.encode()).hexdigest() + '.jpg'
  650. if os.path.exists(cache_file):
  651. return cache_file
  652. if urlparse(song_file).scheme == 'smb':
  653. song_path = config + '/' + os.path.dirname(current['file'])
  654. song_ext = os.path.splitext(current['file'])[1]
  655. remote_song = os.path.basename(current['file'])
  656. smb = simpleSMB(song_path)
  657. if smb.isEnable():
  658. if smb.exists(remote_song):
  659. if smb.copyTo(remote_song, self.TEMP_DIR+'/t'+ song_ext) == False:
  660. return None
  661. song_file = self.TEMP_DIR + '/t'+ song_ext
  662. else:
  663. del smb
  664. return None
  665. else:
  666. del smb
  667. return None
  668. del smb
  669. if os.path.exists(song_file):
  670. r = subprocess.run([self.EXTRACTCA, song_file, cache_file])
  671. if r.returncode == 0:
  672. return cache_file
  673. else:
  674. return None
  675. else:
  676. return None
  677. return None
  678. def getFolderjpg(self, config, current):
  679. if 'file' in current:
  680. song_path = config + '/' + os.path.dirname(current['file'])
  681. if urlparse(song_path).scheme == 'smb':
  682. smb = simpleSMB(song_path)
  683. if smb.isEnable():
  684. aafile = None
  685. if smb.exists('Folder.jpg'):
  686. aafile = 'Folder.jpg'
  687. elif smb.exists('folder.jpg'):
  688. aafile = 'folder.jpg'
  689. elif smb.exists('AlbumArt.jpg'):
  690. aafile = 'AlbumArt.jpg'
  691. elif smb.exists('AlbumArtSmall.jpg'):
  692. aafile = 'AlbumArtSmall.jpg'
  693. if aafile is None:
  694. return None
  695. if smb.copyTo(aafile, self.TEMP_DIR+'/aa.jpg') == False:
  696. del smb
  697. return None
  698. else:
  699. del smb
  700. return (self.TEMP_DIR+'/aa.jpg')
  701. else:
  702. return None
  703. else:
  704. imgfile = None
  705. if os.path.exists( song_path+'/Folder.jpg'):
  706. imgfile = song_path + '/Folder.jpg'
  707. elif os.path.exists(song_path + '/folder.jpg'):
  708. imgfile = song_path + '/folder.jpg'
  709. elif os.path.exists(song_path + '/AlbumArt.jpg'):
  710. imgfile = song_path + '/AlbumArt.jpg'
  711. elif os.path.exists(song_path + '/AlbumArtSmall.jpg'):
  712. imgfile = song_path + '/AlbumArtSmall.jpg'
  713. return imgfile
  714. return None
  715. return None
  716. def draw(self, param):
  717. status = param['status']
  718. current = param['current']
  719. config = param['config']
  720. imgfile = None
  721. if self.use_folderjpg:
  722. if 'file' in current:
  723. path = config + '/' + os.path.dirname(current['file'])
  724. # Album change
  725. if path != self.prev_path:
  726. self.prev_path = path
  727. imgfile = self.getFolderjpg(config, current)
  728. else:
  729. imgfile = self.extractCoverArt(config, current)
  730. if imgfile is None:
  731. if os.path.exists( LOGO_FILE ):
  732. imgfile = LOGO_FILE
  733. # Display AlbumArt
  734. if imgfile is not None:
  735. image = Image.open(imgfile)
  736. param = LCDItem(image, pos=LCDItem.TOP)
  737. msg = Message(self, Message.DISPLAY, param)
  738. self.sendMessage(msg)
  739. return
  740. def receiveMessage(self,message):
  741. if message.getMessage() == Message.MENUOFF:
  742. self.prev_path = '' # メニューが閉じたらアルバムアートをクリア
  743. elif message.getMessage() == Message.MPD:
  744. self.messageq.put(message.getParam())
  745. elif message.getMessage() == Message.TERM:
  746. self.term = True
  747. return True
  748. def run(self):
  749. while self.term == False:
  750. try:
  751. param = self.messageq.get_nowait()
  752. self.draw(param)
  753. except queue.Empty:
  754. sleep(0.05)
  755. return
  756. # SongTitle
  757. class SongTitle(Worker, threading.Thread):
  758. term = False
  759. def __init__(self, pm):
  760. threading.Thread.__init__(self)
  761. Worker.__init__(self, pm)
  762. self.messageq = queue.Queue()
  763. def receiveMessage(self, message):
  764. if message.getMessage() == Message.MPD:
  765. self.messageq.put(message.getParam())
  766. elif message.getMessage() == Message.TERM:
  767. self.term = True
  768. return True
  769. def draw(self, title):
  770. jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.BOTTOM_HEIGHT, encoding="unic");
  771. image = Image.new('RGB', (LCD.WIDTH, LCD.BOTTOM_HEIGHT), 0)
  772. draw = ImageDraw.Draw(image)
  773. draw.text((0,0) , title, font=jpfont, fill='#FFFFFF')
  774. param = LCDItem(image, pos=LCDItem.BOTTOM)
  775. msg = Message(self, Message.DISPLAY, param)
  776. self.sendMessage(msg)
  777. def run(self):
  778. while self.term == False:
  779. try:
  780. param = self.messageq.get_nowait()
  781. if 'title' in param['current']:
  782. self.draw(param['current']['title'])
  783. except queue.Empty:
  784. sleep(0.05)
  785. return
  786. # Indicator worker
  787. class Indicator(Worker, threading.Thread):
  788. status = {'state': 'stop'}
  789. term = False
  790. mute = False
  791. def __init__(self, pm):
  792. threading.Thread.__init__(self)
  793. Worker.__init__(self, pm)
  794. self.messageq = queue.Queue()
  795. self.timer = SDTimer(5, self.refresh)
  796. def refresh(self):
  797. jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.INDICATE_HEIGHT, encoding="unic");
  798. image = Image.new('RGB', (LCD.WIDTH, LCD.INDICATE_HEIGHT), 0)
  799. draw = ImageDraw.Draw(image)
  800. str = ''
  801. if self.mute:
  802. str = 'Mute....'
  803. else:
  804. if self.status['state'] == 'stop':
  805. str = 'Stop'
  806. elif self.status['state'] == 'pause':
  807. str = 'Pause'
  808. else:
  809. if self.status['random'] == '1':
  810. str = str + 'Random'
  811. else:
  812. str = str + 'Normal'
  813. if self.status['repeat'] == '1':
  814. str = str + '/Repeat'
  815. str = str + '/'+ self.status['bitrate'] + 'kbps'
  816. draw.text((0,0) , str, font=jpfont, fill='#00BFFF')
  817. param = LCDItem(image, pos=LCDItem.INDICATE)
  818. msg = Message(self, Message.DISPLAY, param)
  819. self.sendMessage(msg)
  820. return
  821. def volumeBar(self, value):
  822. jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.INDICATE_HEIGHT, encoding="unic");
  823. image = Image.new('RGB', (LCD.WIDTH, LCD.INDICATE_HEIGHT), 0)
  824. draw = ImageDraw.Draw(image)
  825. draw.text((0,0) ,'Vol:', font=jpfont, fill='#7CFC00')
  826. bar_length = int(((LCD.WIDTH - 30) * value) / 100)
  827. draw.rectangle((30, 0, bar_length+30, LCD.INDICATE_HEIGHT), fill=(124, 252, 0))
  828. msgparam = LCDItem(image, pos=LCDItem.INDICATE)
  829. self.sendMessage(Message(self, Message.DISPLAY, msgparam))
  830. return
  831. def receiveMessage(self, message):
  832. if message.getMessage() == Message.MENUOFF:
  833. pass
  834. # self.messageq.put(Message(None, Message.INDICATOR, None))
  835. elif message.getMessage() == Message.INDICATOR:
  836. self.messageq.put(message)
  837. elif message.getMessage() == Message.MPD:
  838. self.status = message.getParam()['status']
  839. self.messageq.put(Message(None, Message.INDICATOR, None))
  840. elif message.getMessage() == Message.VOLUME:
  841. self.messageq.put(message)
  842. elif message.getMessage() == Message.TERM:
  843. self.term = True
  844. return True
  845. def run(self):
  846. while self.term == False:
  847. try:
  848. message = self.messageq.get_nowait()
  849. if message.getMessage() == Message.VOLUME:
  850. param = message.getParam()
  851. if param['type'] == 'volume':
  852. self.volumeBar(int(param['value']))
  853. if self.timer.getActive():
  854. self.timer.setRemaining(5)
  855. else:
  856. self.timer = SDTimer(5, self.refresh)
  857. self.timer.start()
  858. elif param['type'] == 'mute':
  859. self.mute = param['value']
  860. self.refresh()
  861. elif message.getMessage() == Message.INDICATOR:
  862. self.refresh()
  863. except queue.Empty:
  864. sleep(0.05)
  865. if self.timer.getActive():
  866. self.timer.cancel()
  867. return
  868. # MPD Client worker
  869. class MPC(Worker, threading.Thread):
  870. term = False
  871. def __init__(self, pm):
  872. threading.Thread.__init__(self)
  873. Worker.__init__(self, pm)
  874. self.messageq = queue.Queue()
  875. self.semaphore = BoundedSemaphore()
  876. def receiveMessage(self, message):
  877. if message.getMessage() == Message.RCONTROL:
  878. param = {'command': message.getParam(), 'arg': None }
  879. self.messageq.put(param)
  880. elif message.getMessage() == Message.MPC:
  881. self.messageq.put(message.getParam())
  882. elif message.getMessage() == Message.TERM:
  883. self.term = True
  884. return True
  885. def execute(self, param):
  886. self.semaphore.acquire()
  887. try:
  888. mpc = MPDClient(use_unicode=True)
  889. mpc.connect(MPD_HOST, MPD_PORT)
  890. except Exception as e:
  891. print(e, file=self.playermain.out)
  892. self.semaphore.release()
  893. return
  894. try:
  895. if param['command'] == 'playall':
  896. mpc.stop()
  897. mpc.clear()
  898. songs = mpc.list('file')
  899. for song in songs:
  900. mpc.add(song)
  901. mpc.play()
  902. elif param['command'] == 'random':
  903. if param['arg'] == 'on':
  904. mpc.random(1)
  905. else:
  906. mpc.random(0)
  907. elif param['command'] == 'repeat':
  908. if param['arg'] == 'on':
  909. mpc.repeat(1)
  910. else:
  911. mpc.repeat(0)
  912. elif param['command'] == 'update':
  913. image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
  914. font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
  915. draw = ImageDraw.Draw(image)
  916. draw.text((0,64), 'DB更新中....', font=font, fill='#FF0000')
  917. draw.text((0,80), 'お待ち下さい', font=font, fill='#FF0000')
  918. item = LCDItem(image)
  919. self.playermain.lcd.displayItem(item, lock=True)
  920. jobno = mpc.update()
  921. while 'updating_db' in mpc.status():
  922. if mpc.status()['updating_db'] != jobno:
  923. break
  924. sleep(0.1)
  925. self.playermain.lcd.release()
  926. elif param['command'] == 'load':
  927. mpc.stop()
  928. mpc.clear()
  929. mpc.load(param['arg'])
  930. mpc.play()
  931. elif param['command'] == 'list':
  932. list = []
  933. if param['arg'] == 'playlists':
  934. playlists = mpc.listplaylists()
  935. for p in playlists:
  936. list.append(p['playlist'])
  937. elif param['arg'] == 'artist':
  938. list = mpc.list('artist')
  939. elif param['arg'] == 'album':
  940. list = mpc.list('album')
  941. self.sendMessage(Message(self, Message.MPC_REPLY, { 'type': param['arg'], 'list': list }))
  942. elif param['command'] == 'findadd':
  943. mpc.stop()
  944. mpc.clear()
  945. mpc.findadd(param['arg']['type'], param['arg']['value'])
  946. mpc.play()
  947. elif param['command'] == 'delete':
  948. status = mpc.status()
  949. if 'songid' in status:
  950. mpc.deleteid(status['songid'])
  951. elif param['command'] == 'save':
  952. mpc.save(param['arg'])
  953. elif param['command'] == 'play':
  954. mpc.play()
  955. elif param['command'] == 'stop':
  956. mpc.stop()
  957. elif param['command'] == 'pause':
  958. mpc.pause()
  959. elif param['command'] == 'clear':
  960. mpc.clear()
  961. elif param['command'] == 'next':
  962. mpc.next()
  963. elif param['command'] == 'prev':
  964. mpc.previous()
  965. #
  966. except Exception as e:
  967. print(e, file=self.playermain.out)
  968. mpc.close()
  969. self.semaphore.release()
  970. return
  971. def run(self):
  972. while self.term == False:
  973. try:
  974. param = self.messageq.get_nowait()
  975. self.execute(param)
  976. except queue.Empty:
  977. sleep(0.05)
  978. return
  979. # lirc remote control worker
  980. class Remote(Worker, threading.Thread):
  981. def __init__(self, pm):
  982. threading.Thread.__init__(self)
  983. Worker.__init__(self, pm)
  984. lircrc = None
  985. for f in LIRCRC:
  986. if os.path.exists(f):
  987. lircrc = f
  988. break
  989. if lircrc is None:
  990. raise Exception('lircrc not found')
  991. self.sockid = lirc.init("superdac", lircrc, blocking=False)
  992. self.term = False
  993. def receiveMessage(self, message):
  994. if message.getMessage() == Message.TERM:
  995. self.term = True
  996. return True
  997. def run(self):
  998. while self.term == False:
  999. code = lirc.nextcode()
  1000. if len(code) == 0:
  1001. sleep(0.1)
  1002. continue
  1003. message = Message(self, Message.RCONTROL, code[0])
  1004. self.sendMessage(message)
  1005. lirc.deinit()
  1006. return
  1007. # デーモンの終了フラグ
  1008. Terminate = False
  1009. noDaemon = False
  1010. class PlayerMain(Master, threading.Thread):
  1011. global Terminate
  1012. global noDaemon
  1013. workers = [] # type: Worker[]
  1014. out = None
  1015. def __init__(self, err):
  1016. super().__init__()
  1017. self.out = err
  1018. self.messageq = queue.Queue()
  1019. # Message Queuing
  1020. def postMessage(self, message):
  1021. if noDaemon:
  1022. print(message.getMessage() , file=self.out)
  1023. print(message.getParam() , file=self.out)
  1024. self.messageq.put(message)
  1025. return
  1026. def run(self):
  1027. #
  1028. # MPD status watcher
  1029. mi = None
  1030. try:
  1031. mi = MPD(self)
  1032. except Exception as e:
  1033. # MPDが動いていない
  1034. print(e, file=self.out)
  1035. return
  1036. mi.setDaemon(True)
  1037. mi.start()
  1038. self.workers.append(mi)
  1039. # LCD
  1040. self.lcd = LCD(self)
  1041. self.lcd.setDaemon(True)
  1042. self.lcd.start()
  1043. self.workers.append(self.lcd)
  1044. # AlbumArt
  1045. aa = AlbumArt(self)
  1046. aa.setDaemon(True)
  1047. aa.start()
  1048. self.workers.append(aa)
  1049. # リモコン
  1050. try:
  1051. remocon = Remote(self)
  1052. remocon.setDaemon(True)
  1053. remocon.start()
  1054. self.workers.append(remocon)
  1055. except Exception as e:
  1056. print(e, file=self.out)
  1057. # VolumeControl
  1058. vo = VolumeControl(self)
  1059. self.workers.append(vo)
  1060. # MPC
  1061. self.mpc = MPC(self)
  1062. self.mpc.setDaemon(True)
  1063. self.mpc.start()
  1064. self.workers.append(self.mpc)
  1065. # SongTitle
  1066. st = SongTitle(self)
  1067. st.setDaemon(True)
  1068. st.start()
  1069. self.workers.append(st)
  1070. # Indicator
  1071. id = Indicator(self)
  1072. id.setDaemon(True)
  1073. id.start()
  1074. self.workers.append(id)
  1075. # Switches
  1076. sw = Switches(self)
  1077. self.workers.append(sw)
  1078. # SystemMenu
  1079. sm = SysMenu(self)
  1080. self.workers.append(sm)
  1081. # Playlist
  1082. ps = Playlist(self)
  1083. self.workers.append(ps)
  1084. # MainMenu
  1085. mm = MainMenu(self)
  1086. self.workers.append(mm)
  1087. #
  1088. # イベントループ
  1089. #
  1090. while Terminate == False:
  1091. try:
  1092. msg = self.messageq.get_nowait()
  1093. # イベント送出
  1094. for w in self.workers:
  1095. if type(msg.getSender()) != type(w):
  1096. if w.receiveMessage(msg) == False:
  1097. break
  1098. except queue.Empty:
  1099. sleep(0.05)
  1100. # 終了
  1101. for w in self.workers:
  1102. w.receiveMessage(Message(self, Message.TERM, None))
  1103. self.out.close()
  1104. return
  1105. def main_loop():
  1106. global noDaemon
  1107. logfile = sys.stdout
  1108. if not noDaemon:
  1109. try:
  1110. logfile = open(LOGFILE, 'w')
  1111. except Exception as e:
  1112. logfile = sys.stdout
  1113. p = PlayerMain(logfile)
  1114. p.setDaemon(False)
  1115. p.start()
  1116. p.join(None)
  1117. def signal_handler(signal, handler):
  1118. global Terminate
  1119. global noDaemon
  1120. Terminate = True
  1121. if not noDaemon:
  1122. os.remove(PIDFILE)
  1123. def daemonize():
  1124. pid = os.fork()
  1125. if pid > 0:
  1126. pidf = open(PIDFILE, 'w')
  1127. pidf.write(str(pid)+"\n")
  1128. pidf.close()
  1129. sys.exit()
  1130. if pid == 0:
  1131. signal.signal(signal.SIGINT, signal_handler)
  1132. signal.signal(signal.SIGTERM, signal_handler)
  1133. main_loop()
  1134. # Script starts here
  1135. if __name__ == "__main__":
  1136. argv = sys.argv
  1137. if len(argv) > 1:
  1138. if argv[1] == '--nodaemon':
  1139. noDaemon = True
  1140. signal.signal(signal.SIGINT, signal_handler)
  1141. signal.signal(signal.SIGTERM, signal_handler)
  1142. main_loop()
  1143. else:
  1144. daemonize()