123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- #
- # Requires:
- # apt install mpd
- # apt install mpc
- # apt install lirc
- # apt install python3-pip
- # apt install python3-rpi.gpio
- # apt install python3-pil
- # apt install python3-numpy
- # apt install python3-lirc
- # apt install fonts-takao
- # apt install exfat-fuse exfat-utils
- # pip3 install pyalsaaudio
- # pip3 install pysmb
- # Python_ST7735-raspimag.tar.gz
- # python-mpd2.tar.gz
- # stdlibs
- import sys
- import os
- import signal
- import threading
- from threading import BoundedSemaphore
- from threading import Semaphore
- import queue
- import platform
- from select import select
- from time import sleep
- # ABCMeta
- from abc import ABCMeta, abstractmethod
- # Raspberry pi
- import RPi.GPIO as GPIO
- # PIL
- from PIL import Image
- from PIL import ImageDraw
- from PIL import ImageFont
- # Adafruit
- import ST7735 as TFT
- # import Adafruit_GPIO as GPIO
- import Adafruit_GPIO.SPI as SPI
- # lirc
- import lirc
- # mpd
- from mpd import MPDClient
- from socket import error as SocketError
- # alsaaudio
- from alsaaudio import Mixer
- # SMB
- from smb.SMBConnection import SMBConnection
- from nmb.NetBIOS import NetBIOS
- # LCD spec.
- LCD_WIDTH = 128
- LCD_HEIGHT = 160
- SPEED_HZ = 8000000
- # SUPERDAC configration
- DC = 24
- RST = 23
- SPI_PORT = 0
- SPI_DEVICE = 0
- SW1 = 5
- SW2 = 6
- # FONT settings
- DEFAULT_FONT = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
- FONT_SIZE = 12
- # PID File
- PIDFILE='/var/run/superdac.pid'
- LOGFILE='/var/log/superdac.log'
- # MPD Socket
- MPD_HOST='/var/run/mpd/socket'
- MPD_PORT=6600
- # lirc configration files
- LIRCRC = ['/etc/lirc/superdac.lircrc', '/usr/local/etc/superdac.lircrc', './superdac.lircrc' ]
- # Volume file
- VOLUME_FILE='/var/tmp/.superdac.volume'
- # Openning Logo Image
- LOGO_FILE = '/home/mpd/scripts/logo.jpg'
- class SDTimer(threading.Thread):
- __active = False
-
- def __init__(self, sec, callback):
- super(SDTimer, self).__init__()
- self.daemon = True
- self.__sec = sec
- self.__callback = callback
-
- def getRemaining(self):
- return self.__sec
-
- def setRemaining(self, sec):
- self.__sec = sec
-
- def getActive(self):
- return self.__active
-
- def cancel(self):
- self.__active = False
-
- def run(self):
- self.__active = True
-
- while self.__sec != 0:
- sleep(1)
- self.__sec -= 1
- if self.__active == False:
- break
-
- if self.__active:
- self.__callback()
- self.__active = False
-
- return
-
- class castQueue(queue.Queue):
- __depth = 5
- def __init__(self, depth):
- super(castQueue, self).__init__()
- self.__depth = depth
- self.__semaphore = BoundedSemaphore()
-
- def put(self, obj):
- self.__semaphore.acquire()
- while self.qsize() > self.__depth:
- try:
- t = super().get_nowait()
- except queue.Empty:
- pass
- super().put(obj)
- self.__semaphore.release()
- return
-
- def get_nowait(self):
- self.__semaphore.acquire()
- r = None
- try:
- r = super().get_nowait()
- except Exception as e:
- self.__semaphore.release()
- raise e
- self.__semaphore.release()
- return r
-
- # Message class
- class Message():
- TERM = 0 # 'term'
- DISPLAY = 1 # 'disp'
- RCONTROL = 2 # 'rcontrol'
- MENUON = 3 # 'menuon'
- MENUOFF = 4 # 'menuoff'
- MPD = 5 # 'mpd'
- MPC = 6 # 'mpc'
- PLAYLIST = 7 # 'playlist'
- INDICATOR = 8 # 'indicate'
- MPC_REPLY = 9 # 'mpc_reply'
- VOLUME = 10 # 'volume'
-
- def __init__(self, sender, message, param):
- self.__sender = sender
- self.__message = message
- self.__param = param
-
- def getSender(self):
- return self.__sender
-
- def getMessage(self):
- return self.__message
-
- def getParam(self):
- return self.__param
- # LCD Display Item
- class LCDItem():
-
- FULL = 0
- TOP = 1
- INDICATE = 2
- BOTTOM = 3
- def __init__(self, image, clear=False, pos=FULL, time=0):
- self.__image = image
- self.__pos = pos
- self.__time = time
- self.__clear = clear
- return
-
- def getClear(self):
- return self.__clear
-
- def getImage(self):
- return self.__image
-
- def getPos(self):
- return self.__pos
-
- def getTime(self):
- return self.__time
-
- def getWidth(self):
- return self.__image.size[0]
-
- def getHeight(self):
- return self.__image.size[1]
- # Worker Base
- class Worker(metaclass=ABCMeta):
- # type: (Object, Master)
- def __init__(self, pm):
- self.playermain = pm
-
- def sendMessage(self, message):
- self.playermain.postMessage(message)
-
- @abstractmethod
- def receiveMessage(self, message):
- pass
- # Master Base
- class Master(metaclass=ABCMeta):
- @abstractmethod
- def postMessage(self, message):
- pass
- # Switch worker
- class Switches(Worker):
- def __init__(self, pm):
- # type: (Object, Master)
- super().__init__(pm)
-
- GPIO.setmode(GPIO.BCM)
- GPIO.setup(SW1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
- GPIO.setup(SW2, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-
- GPIO.add_event_detect(SW1, GPIO.FALLING, callback=self.swCallback, bouncetime=20)
- GPIO.add_event_detect(SW2, GPIO.FALLING, callback=self.swCallback, bouncetime=20)
-
- def swCallback(self, ch):
- if ch == SW1:
- os.system('/sbin/reboot')
- elif ch == SW2:
- os.system('/sbin/poweroff')
- return
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.TERM:
- GPIO.remove_event_detect(SW1)
- GPIO.remove_event_detect(SW2)
- GPIO.cleanup(SW1)
- GPIO.cleanup(SW2)
-
- return True
- # Playlist manage worker
- class Playlist(Worker):
-
- active = False
- playermain = None
-
- playlists = []
- artists = []
- albums = []
-
- items = []
- current = []
- type = ''
-
- LINES = 10
- LINE_HEIGHT = 16
-
- def __init__(self, pm):
- # type: (Object, Master)
- super().__init__(pm)
-
- # 初期データ取得
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
-
- def makeMenu(self, draw, font):
- self.current = self.items[0:self.LINES-1]
- del self.items[0:self.LINES-1]
- for i in range(0, len(self.current)):
- draw.text((0, i*self.LINE_HEIGHT), str(i+1)+':'+self.current[i], font=font, fill='#FFFFFF' )
- i = i + 1
- if len(self.items) == 0:
- draw.text((0,i*self.LINE_HEIGHT), '0:中止', font=font, fill='#FFFFFF' )
- else:
- draw.text((0,i*self.LINE_HEIGHT), '0:次へ', font=font, fill='#FFFFFF' )
- return
-
- def receiveMessage(self, message):
- # リスト取得
- if message.getMessage() == Message.MPC_REPLY:
- if message.getParam()['type'] == 'playlists':
- self.playlists = message.getParam()['list']
- elif message.getParam()['type'] == 'artist':
- self.artists = message.getParam()['list']
- elif message.getParam()['type'] == 'album':
- self.albums = message.getParam()['list']
- return True
-
- if message.getMessage() == Message.PLAYLIST:
- if not self.active:
- self.type = message.getParam()
- if self.type == 'playlists':
- self.items = self.playlists
- elif self.type == 'artist':
- self.items = self.artists
- elif self.type == 'album':
- self.items = self.albums
-
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- # プレイリストが空
- if len(self.items) == 0:
- draw.text((0,64), 'プレイリストが', font=font, fill='#FF0000')
- draw.text((0,80), ' ありません', font=font, fill='#FF0000')
- item = LCDItem(image, time=2)
- self.sendMessage(Message(self, Message.DISPLAY, item))
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- return False
- else:
- # 新規メニュー描画
- self.makeMenu(draw, font)
- item = LCDItem(image)
- self.playermain.lcd.displayItem(item, lock=True)
- self.active = True
-
- return False
- return False
- # リモコン
- if message.getMessage() == Message.RCONTROL and self.active:
- # 数字ボタン
- if message.getParam() in '0123456789':
- btn = int(message.getParam()) - 1
- if btn < 0:
- if len(self.items) == 0:
- # 終了
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- self.playermain.lcd.release()
- else:
- # 次ページ
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- self.makeMenu(draw, font)
- item = LCDItem(image)
- self.playermain.lcd.release()
- self.playermain.lcd.displayItem(item, lock=True)
- self.active = True
- return False
- elif btn < len(self.current):
- if self.type == 'playlists':
- self.sendMessage(Message(self, Message.MPC, {'command': 'load', 'arg': self.current[btn]}))
- else:
- param = {'command':'findadd', 'arg': {'type':self.type,'value': self.current[btn]}}
- msg = Message(self, Message.MPC, param)
- self.sendMessage(msg)
- self.playermain.lcd.release()
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- self.active = False
- return True
- return True
- # メインメニューに戻る
- elif message.getParam() == 'menu':
- self.playermain.lcd.release()
- self.active = False
-
- return True
- return True
- # MainMenu Worker
- class MainMenu(Worker):
-
- active = False
- playermain = None # type: Master
- status = []
- playlists = []
-
- def __init__(self, pm):
- # type: (Object, Master)
- super().__init__(pm)
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
-
- def receiveMessage(self,message):
- # MPD Message
- if message.getMessage() == Message.MPD:
- param = message.getParam()
- self.status = param['status']
- return True
-
- if message.getMessage() == Message.MPC_REPLY:
- if message.getParam()['type'] == 'playlists':
- self.playlists = message.getParam()['list']
- return True
-
- # リモコン以外は無視
- if message.getMessage() != Message.RCONTROL:
- return True
-
- # Remote Control Message
- if message.getParam() == 'menu':
- # おそらくありえない
- if len(self.status) == 0:
- return True
-
- # アクティブなら閉じる
- if self.active:
- self.active = False
- self.playermain.lcd.release()
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- return True
-
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists'} ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
- # メニュー描画
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- if self.status['random'] == '1':
- draw.text((0,0),'1:ノーマル再生', font=font,fill='#FFFFFF')
- else:
- draw.text((0,0),'1:ランダム再生', font=font,fill='#FFFFFF')
-
- if self.status['repeat'] == '1':
- draw.text((0,16),'2:リピートしない', font=font,fill='#FFFFFF')
- else:
- draw.text((0,16),'2:リピート再生', font=font,fill='#FFFFFF')
-
- draw.text((0,32 ), '3:現在の曲を外す', font=font,fill='#FFFFFF')
- draw.text((0,48 ), '4:プレイリスト保存', font=font,fill='#FFFFFF')
- draw.text((0,64 ), '5:全曲再生', font=font,fill='#FFFFFF')
- draw.text((0,80 ), '6:プレイリスト読込', font=font,fill='#FFFFFF')
-
- draw.text((0,96 ), '7:アルバム選択', font=font,fill='#FFFFFF')
- draw.text((0,112), '8:アーティスト選択', font=font,fill='#FFFFFF')
-
- draw.text((0,128), '9:DBアップデート', font=font,fill='#FFFFFF')
-
- # self.sendMessage(Message(self, Message.MENUON, None ))
- item = LCDItem(image, pos=LCDItem.FULL)
- self.playermain.lcd.displayItem(item,lock=True)
- self.active = True
- return True
-
- # アクティブかつリモコンが押された
- if self.active:
- c = message.getParam()
- if c in '0123456789':
- if c == '1': # random
- arg = 'on'
- if self.status['random'] == '1':
- arg = 'off'
- self.sendMessage(Message(self, Message.MPC, {'command':'random', 'arg': arg} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '2': # repeat
- arg = 'on'
- if self.status['repeat'] == '1':
- arg = 'off'
- self.sendMessage(Message(self, Message.MPC, {'command':'repeat', 'arg': arg} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '3': # delete
- self.sendMessage(Message(self, Message.MPC, {'command':'delete', 'arg': None} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '4': # save
- for i in range(0,100):
- if not (('playlist'+str(i)) in self.playlists):
- break
- self.sendMessage(Message(self, Message.MPC, {'command':'save', 'arg': 'playlist'+str(i)}))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '5': # play all
- self.sendMessage(Message(self, Message.MPC, {'command':'playall', 'arg': None} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
-
- elif c == '6': # playlist
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'playlists' } ))
- self.sendMessage(Message(self, Message.PLAYLIST, 'playlists' ))
- self.active = False
-
- elif c == '7': # album
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'album' } ))
- self.sendMessage(Message(self, Message.PLAYLIST, 'album' ))
- self.active = False
-
- elif c == '8': # artist
- self.sendMessage(Message(self, Message.MPC, {'command':'list', 'arg': 'artist' } ))
- self.sendMessage(Message(self, Message.PLAYLIST, 'artist' ))
- self.active = False
-
- elif c == '9': # db update
- self.sendMessage(Message(self, Message.MPC, {'command':'update', 'arg': None} ))
- self.active = False
- self.sendMessage(Message(self, Message.MENUOFF, None ))
- if self.active == False:
- self.playermain.lcd.release()
- return False
- return True
- return True
- # Volume Control Worker
- class VolumeControl(Worker):
-
- MAX_VALUE = 81 # 81% == 0dB
- enabled = True
- term = False
- value = 100
-
- mute = False
-
- def __init__(self, pm):
- super().__init__(pm)
-
- # read initial value
- if os.path.exists(VOLUME_FILE):
- with open(VOLUME_FILE, "r") as f:
- try:
- self.value = int(f.read())
- except Exception as e:
- pass
-
- try:
- self.mixer = Mixer('Digital')
- except Exception as e:
- print(e, file=self.playermain.out)
- self.enabled = False
-
- if self.enabled:
- self.mixer.setvolume(int((self.MAX_VALUE *self.value)/100))
- self.mixer.setmute(self.mute)
-
- def receiveMessage(self,message):
- if message.getMessage() == Message.RCONTROL:
- # enabled ?
- if not self.enabled:
- return True
- if message.getParam() == 'volup':
- self.value = self.value + 2
- if self.value > 100:
- self.value = 100
- elif message.getParam() == 'voldown':
- self.value = self.value - 2
- if self.value < 0:
- self.value = 0
- elif message.getParam() == 'mute':
- self.mute = not(self.mute)
- self.mixer.setmute(self.mute)
- self.sendMessage(Message(self, Message.VOLUME, {'type':'mute', 'value': self.mute}))
- if message.getParam() == 'volup' or message.getParam() == 'voldown':
- self.mixer.setvolume(int((self.MAX_VALUE*self.value)/100))
- self.sendMessage(Message(self, Message.VOLUME, {'type':'volume', 'value': self.value}))
- # Terminate
- elif message.getMessage() == Message.TERM:
- try:
- with open(VOLUME_FILE, "w") as f:
- f.write(str(self.value))
- except Exception as e:
- pass
-
- return True
- # LCD Worker
- class LCD(Worker, threading.Thread):
- term = False
-
- HEIGHT = LCD_HEIGHT
- WIDTH = LCD_WIDTH
-
- BOTTOM_HEIGHT = 12
- INDICATE_HEIGHT = 10
-
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self,pm)
-
- self.spi = SPI.SpiDev(SPI_PORT, SPI_DEVICE,max_speed_hz=SPEED_HZ)
- self.disp = TFT.ST7735(DC, rst=RST, spi=self.spi)
- self.disp.begin()
- self.disp.clear((0,0,0));
- if os.path.exists(LOGO_FILE):
- image = Image.open(LOGO_FILE)
- self.disp.buffer.paste(image)
- self.disp.display()
- self.messageq = castQueue(5)
- self.semaphore = BoundedSemaphore()
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.DISPLAY:
- self.messageq.put(message.getParam())
- return False
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def release(self):
- try:
- self.semaphore.release()
- except Exception as e:
- print(e, file=self.playermain.out)
-
- def displayItem(self, item, lock=False):
- self.semaphore.acquire()
- try:
- # 画面全体に貼り付け
- if item.getPos() == LCDItem.FULL:
- if item.getClear():
- self.disp.clear((0,0,0))
- if item.getImage() is not None:
- self.disp.buffer.paste(item.getImage())
- self.disp.display()
- else:
- left = 0
- upper = 0
- item_width = item.getWidth()
- item_height = item.getHeight()
- image = item.getImage()
- # サイズ制限
- if item.getWidth() != self.WIDTH:
- scale = float(float(self.WIDTH)/float(item.getWidth()))
- image = item.getImage().resize((int(item_width * scale), int(item_height*scale)), Image.BILINEAR)
- if image.size[1] > self.HEIGHT:
- image = image.crop( box=(0,0,self.WIDTH, self.HEIGHT) )
-
- # Display TOP
- if item.getPos() == LCDItem.TOP:
- self.disp.display(image, x0=left, y0=upper, x1=self.WIDTH-1, y1=image.size[1]-1 )
-
- # Display Bottom
- elif item.getPos() == LCDItem.BOTTOM:
- if image.size[1] > self.BOTTOM_HEIGHT:
- image = image.crop( box=(0, 0, self.WIDTH, self.BOTTOM_HEIGHT) )
- self.disp.display(image, x0=left, y0 = self.HEIGHT-self.BOTTOM_HEIGHT-1, x1=self.WIDTH-1, y1=self.HEIGHT-1 )
-
- # Display Indicator
- elif item.getPos() == LCDItem.INDICATE:
- if image.size[1] > self.INDICATE_HEIGHT:
- image = image.crop( box=(0, 0, self.WIDTH, self.INDICATE_HEIGHT) )
- 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 )
-
- if item.getTime() != 0:
- sleep(item.getTime())
- except Exception as e: # 何かエラーが起きたら無条件で開放
- print(e, file=self.playermain.out)
- self.release()
- return
-
- if lock == False:
- self.release()
- return
-
- def run(self):
- while self.term == False:
- try:
- # LCDItem
- item = self.messageq.get_nowait()
- self.displayItem(item)
- except queue.Empty:
- sleep(0.05)
-
- return True
- # MPD state watch worker
- class MPD(Worker, threading.Thread):
-
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self,pm)
-
- try:
- self.mpc = MPDClient(use_unicode=True)
- self.mpc.connect( MPD_HOST, MPD_PORT );
- except SocketError as e:
- print(e, file=self.playermain.out);
- return
-
- self.config = self.mpc.config()
- current = self.mpc.currentsong()
- status = self.mpc.status()
- self.param = {'changes':None, 'current': current, 'status': status, 'config': self.config }
-
- def receiveMessage(self,message):
- if message.getMessage() == Message.TERM:
- self.term = True
- elif message.getMessage() == Message.MENUOFF:
- self.sendMessage()
-
- return True
-
- def sendMessage(self):
- super().sendMessage(Message(self, Message.MPD, self.param))
- return
-
- def run(self):
- # 現状レポート
- current = self.mpc.currentsong()
- status = self.mpc.status()
- self.param = {'changes':None, 'current': current, 'status': status, 'config': self.config }
- self.sendMessage()
-
- self.mpc.send_idle()
- while self.term == False:
- canRead = select([self.mpc], [], [], 0)[0]
- if canRead:
- changes = self.mpc.fetch_idle()
- current = self.mpc.currentsong()
- status = self.mpc.status()
- self.param = {'changes': changes, 'current': current, 'status': status, 'config': self.config }
- self.sendMessage()
- self.mpc.send_idle()
- sleep(0.1)
- # idle中にCloseすると怒られる
- # self.mpc.close()
- return
- # Album art worker
- class AlbumArt(Worker, threading.Thread):
- TEMP_DIR = '/var/tmp/.superdac.aa'
- term = False
- refresh = False
- prev_path = 'foobar'
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self,pm)
- self.messageq = queue.Queue()
-
- def getAlbumArt(self, config, current):
- param = config.split('/')
- if param[0] == 'smb:':
- # SMB
- musicfile = (current['file']).split('/')
- remote_host = param[2]
- share_name = param[3]
-
- remote_path = '/'
- for r in param[4:]:
- remote_path = remote_path + r + '/'
- for r in musicfile[0:len(musicfile)-1]:
- remote_path = remote_path + r + '/'
-
- try:
- nmb = NetBIOS()
- remote_ip = nmb.queryName(remote_host)[0]
- if remote_ip is None: # 名前が引けない
- return None
-
- con = SMBConnection('','',platform.uname().node, remote_host)
- con.connect(remote_ip)
- items = con.listPath(share_name, remote_path)
- files = [item.filename for item in items]
- aafile = ''
- if 'Folder.jpg' in files:
- aafile = remote_path + 'Folder.jpg'
- elif 'folder.jpg' in files:
- aafile = remote_Path + 'folder.jpg'
- elif 'AlbumArt.jpg' in files:
- aafile = remote_path + 'AlbumArt.jpg'
- elif 'AlbumArtSmall.jpg' in files:
- aafile = remote_path + 'AlbumArtSmall.jpg'
- else:
- con.close()
- return None
-
- if not os.path.exists(self.TEMP_DIR):
- os.mkdir(self.TEMP_DIR)
-
- with open( self.TEMP_DIR+'/aa.jpg', 'wb') as f:
- con.retrieveFile(share_name, aafile, f)
-
- con.close()
- except Exception as e:
- print(e, file=self.playermain.out)
- return None
- return self.TEMP_DIR+'/aa.jpg'
- else:
- path = config + '/' + os.path.dirname(current['file'])
- imgfile = None
- if os.path.exists( path+'/Folder.jpg'):
- imgfile = path + '/Folder.jpg'
- elif os.path.exists(path + '/folder.jpg'):
- imgfile = path + '/folder.jpg'
- elif os.path.exists(path + '/AlbumArt.jpg'):
- imgfile = path + '/AlbumArt.jpg'
- elif os.path.exists(path + '/AlbumArtSmall.jpg'):
- imgfile = path + '/AlbumArtSmall.jpg'
- return imgfile
- return None
-
- def draw(self, param):
- status = param['status']
- current = param['current']
- config = param['config']
-
- imgfile = None
- if 'file' in current:
- path = config + '/' + os.path.dirname(current['file'])
- # Album change
- if path != self.prev_path:
- self.prev_path = path
- imgfile = self.getAlbumArt(config, current)
- # アルバムアートがなければロゴ
- if imgfile is None:
- if os.path.exists(LOGO_FILE):
- imgfile = LOGO_FILE
- else: # 停止中かなにか
- if imgfile is None:
- if os.path.exists(LOGO_FILE):
- imgfile = LOGO_FILE
-
- # Display AlbumArt
- if imgfile is not None:
- # LCD clear
- param = LCDItem(None, clear=True)
- msg = Message(self,Message.DISPLAY, param)
- self.sendMessage(msg)
- #
- image = Image.open(imgfile)
- param = LCDItem(image, pos=LCDItem.TOP)
- msg = Message(self, Message.DISPLAY, param)
- self.sendMessage(msg)
- # インジケータ再描画
- self.sendMessage(Message(self, Message.INDICATOR, None))
-
- # Display Title
- if 'title' in current:
- title = current['title']
- jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.BOTTOM_HEIGHT, encoding="unic");
- image = Image.new('RGB', (LCD.WIDTH, LCD.BOTTOM_HEIGHT), 0)
- draw = ImageDraw.Draw(image)
- draw.text((0,0) , title, font=jpfont, fill='#FFFFFF')
- param = LCDItem(image, pos=LCDItem.BOTTOM)
- msg = Message(self, Message.DISPLAY, param)
- self.sendMessage(msg)
- return
-
- def receiveMessage(self,message):
- if message.getMessage() == Message.MENUOFF:
- self.prev_path = '' # メニューが閉じたらアルバムアートをクリア
- elif message.getMessage() == Message.MPD:
- self.messageq.put(message.getParam())
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def run(self):
- while self.term == False:
- try:
- param = self.messageq.get_nowait()
- self.draw(param)
- except queue.Empty:
- sleep(0.05)
- return
-
- # Indicator worker
- class Indicator(Worker, threading.Thread):
-
- status = {'state': 'stop'}
- term = False
- mute = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self, pm)
-
- self.messageq = queue.Queue()
- self.timer = SDTimer(5, self.refresh)
-
- def refresh(self):
- jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.INDICATE_HEIGHT, encoding="unic");
- image = Image.new('RGB', (LCD.WIDTH, LCD.INDICATE_HEIGHT), 0)
- draw = ImageDraw.Draw(image)
- str = ''
- if self.mute:
- str = 'Mute....'
- else:
- if self.status['state'] == 'stop':
- str = 'Stop'
- elif self.status['state'] == 'pause':
- str = 'Pause'
- else:
- if self.status['random'] == '1':
- str = str + 'Random'
- else:
- str = str + 'Normal'
- if self.status['repeat'] == '1':
- str = str + '/Repeat'
- str = str + '/'+ self.status['bitrate'] + 'kbps'
-
- draw.text((0,0) , str, font=jpfont, fill='#00BFFF')
- param = LCDItem(image, pos=LCDItem.INDICATE)
- msg = Message(self, Message.DISPLAY, param)
- self.sendMessage(msg)
- return
-
- def volumeBar(self, value):
- jpfont = ImageFont.truetype(DEFAULT_FONT, LCD.INDICATE_HEIGHT, encoding="unic");
- image = Image.new('RGB', (LCD.WIDTH, LCD.INDICATE_HEIGHT), 0)
- draw = ImageDraw.Draw(image)
- draw.text((0,0) ,'Vol:', font=jpfont, fill='#7CFC00')
- bar_length = int(((LCD.WIDTH - 30) * value) / 100)
- draw.rectangle((30, 0, bar_length+30, LCD.INDICATE_HEIGHT), fill=(124, 252, 0))
- msgparam = LCDItem(image, pos=LCDItem.INDICATE)
- self.sendMessage(Message(self, Message.DISPLAY, msgparam))
- return
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.MENUOFF:
- pass
- # self.messageq.put(Message(None, Message.INDICATOR, None))
- elif message.getMessage() == Message.INDICATOR:
- self.messageq.put(message)
- elif message.getMessage() == Message.MPD:
- self.status = message.getParam()['status']
- self.messageq.put(Message(None, Message.INDICATOR, None))
- elif message.getMessage() == Message.VOLUME:
- self.messageq.put(message)
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def run(self):
- while self.term == False:
- try:
- message = self.messageq.get_nowait()
- if message.getMessage() == Message.VOLUME:
- param = message.getParam()
- if param['type'] == 'volume':
- self.volumeBar(int(param['value']))
- if self.timer.getActive():
- self.timer.setRemaining(5)
- else:
- self.timer = SDTimer(5, self.refresh)
- self.timer.start()
- elif param['type'] == 'mute':
- self.mute = param['value']
- self.refresh()
- elif message.getMessage() == Message.INDICATOR:
- self.refresh()
- except queue.Empty:
- sleep(0.05)
- if self.timer.getActive():
- self.timer.cancel()
- return
- # MPD Client worker
- class MPC(Worker, threading.Thread):
- term = False
-
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self, pm)
-
- self.messageq = queue.Queue()
- self.semaphore = BoundedSemaphore()
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.RCONTROL:
- param = {'command': message.getParam(), 'arg': None }
- self.messageq.put(param)
- elif message.getMessage() == Message.MPC:
- self.messageq.put(message.getParam())
- elif message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def execute(self, param):
- self.semaphore.acquire()
- try:
- mpc = MPDClient(use_unicode=True)
- mpc.connect(MPD_HOST, MPD_PORT)
- except Exception as e:
- print(e, file=self.playermain.out)
- self.semaphore.release()
- return
-
- try:
- if param['command'] == 'playall':
- mpc.stop()
- mpc.clear()
- songs = mpc.list('file')
- for song in songs:
- mpc.add(song)
- mpc.play()
-
- elif param['command'] == 'random':
- if param['arg'] == 'on':
- mpc.random(1)
- else:
- mpc.random(0)
-
- elif param['command'] == 'repeat':
- if param['arg'] == 'on':
- mpc.repeat(1)
- else:
- mpc.repeat(0)
-
- elif param['command'] == 'update':
- image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0)
- font = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic");
- draw = ImageDraw.Draw(image)
- draw.text((0,64), 'DB更新中....', font=font, fill='#FF0000')
- draw.text((0,80), 'お待ち下さい', font=font, fill='#FF0000')
- item = LCDItem(image)
- self.playermain.lcd.displayItem(item, lock=True)
- jobno = mpc.update()
- while 'updating_db' in mpc.status():
- if mpc.status()['updating_db'] != jobno:
- break
- sleep(0.1)
- self.playermain.lcd.release()
-
- elif param['command'] == 'load':
- mpc.stop()
- mpc.clear()
- mpc.load(param['arg'])
- mpc.play()
-
- elif param['command'] == 'list':
- list = []
- if param['arg'] == 'playlists':
- playlists = mpc.listplaylists()
- for p in playlists:
- list.append(p['playlist'])
- elif param['arg'] == 'artist':
- list = mpc.list('artist')
- elif param['arg'] == 'album':
- list = mpc.list('album')
- self.sendMessage(Message(self, Message.MPC_REPLY, { 'type': param['arg'], 'list': list }))
-
- elif param['command'] == 'findadd':
- mpc.stop()
- mpc.clear()
- mpc.findadd(param['arg']['type'], param['arg']['value'])
- mpc.play()
-
- elif param['command'] == 'delete':
- status = mpc.status()
- if 'songid' in status:
- mpc.deleteid(status['songid'])
-
- elif param['command'] == 'save':
- mpc.save(param['arg'])
-
- elif param['command'] == 'play':
- mpc.play()
-
- elif param['command'] == 'stop':
- mpc.stop()
-
- elif param['command'] == 'pause':
- mpc.pause()
-
- elif param['command'] == 'clear':
- mpc.clear()
-
- elif param['command'] == 'next':
- mpc.next()
-
- elif param['command'] == 'prev':
- mpc.previous()
- #
- except Exception as e:
- print(e, file=self.playermain.out)
-
- mpc.close()
- self.semaphore.release()
- return
-
- def run(self):
- while self.term == False:
- try:
- param = self.messageq.get_nowait()
- self.execute(param)
- except queue.Empty:
- sleep(0.05)
-
- return
-
- # lirc remote control worker
- class Remote(Worker, threading.Thread):
- def __init__(self, pm):
- threading.Thread.__init__(self)
- Worker.__init__(self, pm)
-
- lircrc = None
- for f in LIRCRC:
- if os.path.exists(f):
- lircrc = f
- break
- if lircrc is None:
- raise Exception('lircrc not found')
-
- self.sockid = lirc.init("superdac", lircrc, blocking=False)
- self.term = False
-
- def receiveMessage(self, message):
- if message.getMessage() == Message.TERM:
- self.term = True
- return True
-
- def run(self):
- while self.term == False:
- code = lirc.nextcode()
- if len(code) == 0:
- sleep(0.1)
- continue
- message = Message(self, Message.RCONTROL, code[0])
- self.sendMessage(message)
-
- lirc.deinit()
- return
- # デーモンの終了フラグ
- Terminate = False
- noDaemon = False
- class PlayerMain(Master, threading.Thread):
- global Terminate
- global noDaemon
-
- workers = [] # type: Worker[]
- out = None
-
- def __init__(self, err):
- super().__init__()
- self.out = err
- self.messageq = queue.Queue()
-
- # Message Queuing
- def postMessage(self, message):
- if noDaemon:
- print(message.getMessage() , file=self.out)
- print(message.getParam() , file=self.out)
- self.messageq.put(message)
- return
-
- def run(self):
- #
- # MPD status watcher
- mi = None
- try:
- mi = MPD(self)
- except Exception as e:
- # MPDが動いていない
- print(e, file=self.out)
- return
- mi.setDaemon(True)
- mi.start()
- self.workers.append(mi)
-
- # LCD
- self.lcd = LCD(self)
- self.lcd.setDaemon(True)
- self.lcd.start()
- self.workers.append(self.lcd)
-
- # AlbumArt
- aa = AlbumArt(self)
- aa.setDaemon(True)
- aa.start()
- self.workers.append(aa)
-
- # リモコン
- try:
- remocon = Remote(self)
- remocon.setDaemon(True)
- remocon.start()
- self.workers.append(remocon)
- except Exception as e:
- print(e, file=self.out)
-
- # VolumeControl
- vo = VolumeControl(self)
- self.workers.append(vo)
-
- # MPC
- self.mpc = MPC(self)
- self.mpc.setDaemon(True)
- self.mpc.start()
- self.workers.append(self.mpc)
-
- # Indicator
- id = Indicator(self)
- id.setDaemon(True)
- id.start()
- self.workers.append(id)
-
- # Switches
- sw = Switches(self)
- self.workers.append(sw)
-
- # Playlist
- ps = Playlist(self)
- self.workers.append(ps)
-
- # MainMenu
- mm = MainMenu(self)
- self.workers.append(mm)
-
-
- #
- # イベントループ
- #
- while Terminate == False:
- try:
- msg = self.messageq.get_nowait()
- # イベント送出
- for w in self.workers:
- if type(msg.getSender()) != type(w):
- if w.receiveMessage(msg) == False:
- break
-
- except queue.Empty:
- sleep(0.05)
-
- # 終了
- for w in self.workers:
- w.receiveMessage(Message(self, Message.TERM, None))
-
- self.out.close()
- return
-
- def main_loop():
- global noDaemon
-
- logfile = sys.stdout
- if not noDaemon:
- try:
- logfile = open(LOGFILE, 'w')
- except Exception as e:
- logfile = sys.stdout
-
- p = PlayerMain(logfile)
- p.setDaemon(False)
- p.start()
- p.join(None)
-
- def signal_handler(signal, handler):
- global Terminate
- global noDaemon
-
- Terminate = True
- if not noDaemon:
- os.remove(PIDFILE)
- def daemonize():
- pid = os.fork()
- if pid > 0:
- pidf = open(PIDFILE, 'w')
- pidf.write(str(pid)+"\n")
- pidf.close()
- sys.exit()
-
- if pid == 0:
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
- main_loop()
-
- # Script starts here
- if __name__ == "__main__":
- argv = sys.argv
- if len(argv) > 1:
- if argv[1] == '--nodaemon':
- noDaemon = True
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
- main_loop()
- else:
- daemonize()
|