#!/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()