#!/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 from urllib.parse import urlparse import subprocess import hashlib import glob # 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 # sdlib from sdlib import simpleSMB from sdlib import castQueue from sdlib import SDTimer # 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' # 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' SYSMENU = 11 # 'sysmenu' 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 # system menu class SysMenu(Worker): active = False def __init__(self, pm): super().__init__(pm) def receiveMessage(self, message): if message.getMessage() == Message.SYSMENU: if self.active == False: self.active = True image = Image.new('RGB', (LCD.WIDTH, LCD.HEIGHT), 0) jpfont = ImageFont.truetype(DEFAULT_FONT, 14, encoding="unic"); draw = ImageDraw.Draw(image) draw.text((0,0) , '1:再起動' , font=jpfont, fill='#FFFFFF') draw.text((0,16) , '2:シャットダウン', font=jpfont, fill='#FFFFFF') draw.text((0,32) , '3:キャッシュ消去', font=jpfont, fill='#FFFFFF') draw.text((0,144), '0:中止' , font=jpfont, fill='#FFFFFF') item = LCDItem(image) self.playermain.lcd.displayItem(item, lock=True) return False elif message.getMessage() == Message.RCONTROL and self.active: c = message.getParam() if c in '0123': if c == '1': # reboot os.system('/sbin/reboot') self.active = False self.sendMessage(Message(self, Message.MENUOFF, None )) elif c == '2': # shutdown os.system('/sbin/poweroff') self.active = False self.sendMessage(Message(self, Message.MENUOFF, None )) elif c == '3': # cache clear cache_dir = AlbumArt.TEMP_DIR [os.remove(f) for f in glob.glob(cache_dir+'/*' )] self.active = False self.sendMessage(Message(self, Message.MENUOFF, None )) elif c == '0': # cacnel self.active = False self.sendMessage(Message(self, Message.MENUOFF, None )) elif c == 'menu': # メインメニューに戻る self.active = False self.playermain.lcd.release() return True if self.active == False: self.playermain.lcd.release() return 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') draw.text((0,144), '0:システムメニュー', font=font,fill='#FFFFFF') 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 )) elif c == '0': # sysmenu self.sendMessage(Message(self, Message.SYSMENU, None)) self.active = False 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 TOP_HEIGHT = 138 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) # Display TOP if item.getPos() == LCDItem.TOP: topimg = Image.new('RGB', (LCD.WIDTH, LCD.TOP_HEIGHT), 0) topimg.paste(image) self.disp.display(topimg, x0=left, y0=upper, x1=self.WIDTH-1, y1=topimg.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' EXTRACTCA = '/usr/local/bin/extractCoverArt' term = False refresh = False prev_path = 'foobar' term = False use_folderjpg = True def __init__(self, pm): threading.Thread.__init__(self) Worker.__init__(self,pm) self.messageq = queue.Queue() if not os.path.exists(self.TEMP_DIR): os.mkdir(self.TEMP_DIR) if os.path.exists(self.EXTRACTCA): self.use_folderjpg = False def extractCoverArt(self, config, current): if 'file' in current: song_file = config + '/' + current['file'] cache_file = self.TEMP_DIR + '/' + hashlib.sha256(song_file.encode()).hexdigest() + '.jpg' if os.path.exists(cache_file): return cache_file if urlparse(song_file).scheme == 'smb': song_path = config + '/' + os.path.dirname(current['file']) song_ext = os.path.splitext(current['file'])[1] remote_song = os.path.basename(current['file']) smb = simpleSMB(song_path) if smb.isEnable(): if smb.exists(remote_song): if smb.copyTo(remote_song, self.TEMP_DIR+'/t'+ song_ext) == False: return None song_file = self.TEMP_DIR + '/t'+ song_ext else: del smb return None else: del smb return None del smb if os.path.exists(song_file): r = subprocess.run([self.EXTRACTCA, song_file, cache_file]) if r.returncode == 0: return cache_file else: return None else: return None return None def getFolderjpg(self, config, current): if 'file' in current: song_path = config + '/' + os.path.dirname(current['file']) if urlparse(song_path).scheme == 'smb': smb = simpleSMB(song_path) if smb.isEnable(): aafile = None if smb.exists('Folder.jpg'): aafile = 'Folder.jpg' elif smb.exists('folder.jpg'): aafile = 'folder.jpg' elif smb.exists('AlbumArt.jpg'): aafile = 'AlbumArt.jpg' elif smb.exists('AlbumArtSmall.jpg'): aafile = 'AlbumArtSmall.jpg' if aafile is None: return None if smb.copyTo(aafile, self.TEMP_DIR+'/aa.jpg') == False: del smb return None else: del smb return (self.TEMP_DIR+'/aa.jpg') else: return None else: imgfile = None if os.path.exists( song_path+'/Folder.jpg'): imgfile = song_path + '/Folder.jpg' elif os.path.exists(song_path + '/folder.jpg'): imgfile = song_path + '/folder.jpg' elif os.path.exists(song_path + '/AlbumArt.jpg'): imgfile = song_path + '/AlbumArt.jpg' elif os.path.exists(song_path + '/AlbumArtSmall.jpg'): imgfile = song_path + '/AlbumArtSmall.jpg' return imgfile return None return None def draw(self, param): status = param['status'] current = param['current'] config = param['config'] imgfile = None if self.use_folderjpg: if 'file' in current: path = config + '/' + os.path.dirname(current['file']) # Album change if path != self.prev_path: self.prev_path = path imgfile = self.getFolderjpg(config, current) else: imgfile = self.extractCoverArt(config, current) if imgfile is None: if os.path.exists( LOGO_FILE ): imgfile = LOGO_FILE # Display AlbumArt if imgfile is not None: image = Image.open(imgfile) param = LCDItem(image, pos=LCDItem.TOP) 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 # SongTitle class SongTitle(Worker, threading.Thread): term = False def __init__(self, pm): threading.Thread.__init__(self) Worker.__init__(self, pm) self.messageq = queue.Queue() def receiveMessage(self, message): if message.getMessage() == Message.MPD: self.messageq.put(message.getParam()) elif message.getMessage() == Message.TERM: self.term = True return True def draw(self, 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) def run(self): while self.term == False: try: param = self.messageq.get_nowait() if 'title' in param['current']: self.draw(param['current']['title']) 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) # SongTitle st = SongTitle(self) st.setDaemon(True) st.start() self.workers.append(st) # Indicator id = Indicator(self) id.setDaemon(True) id.start() self.workers.append(id) # Switches sw = Switches(self) self.workers.append(sw) # SystemMenu sm = SysMenu(self) self.workers.append(sm) # 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()