This commit is contained in:
barry 2025-01-10 15:50:43 +01:00
parent f32f606e56
commit 0bc8f50443
5 changed files with 156 additions and 87 deletions

View File

@ -3,7 +3,7 @@ repos:
rev: v0.9.0 # Use the desired version of ruff
hooks:
- id: ruff
files: spotiplayer_pi/
args: ["--ignore=E711,E721"]
# args: ["--fix"] # Automatically apply fixes where possible
- id: ruff-format
files: spotiplayer_pi/
args: ["--ignore=E711,E721"]

View File

@ -1,4 +1,6 @@
import requests
import warnings
class Api:
def __init__(self, refresh_token: str, base_64: str):
@ -13,11 +15,13 @@ class Api:
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
}
req = requests.post(uri, data=data, headers={"Authorization": "Basic " + self.base_64}).json()
req = requests.post(
uri, data=data, headers={"Authorization": "Basic " + self.base_64}
).json()
self.access_token = req["access_token"]
self.header = {"Authorization": f"Bearer {self.access_token}"}
return req["expires_in"]
def getPlaying(self):
url = "https://api.spotify.com/v1/me/player/currently-playing"
req = requests.get(url, headers=self.header)
@ -42,8 +46,7 @@ class Api:
if img_urls:
res["img_url"] = img_urls.pop()["url"]
else:
warnings.warn(f"{res['track']} - {res['artists']}\nAlbum art can't be found\n{img_urls}")
warnings.warn(
f"{res['track']} - {res['artists']}\nAlbum art can't be found\n{img_urls}"
)
return res

View File

@ -27,31 +27,40 @@
# THE SOFTWARE.
#
import os
import sys
import time
import spidev
import logging
import numpy as np
class RaspberryPi:
def __init__(self,spi_freq=40000000,rst=27,dc=25,bl=18,bl_freq=1000,i2c=None,i2c_freq=100000):
def __init__(
self,
spi_freq=40000000,
rst=27,
dc=25,
bl=18,
bl_freq=1000,
i2c=None,
i2c_freq=100000,
):
import RPi.GPIO
self.np = np
self.RST_PIN= rst
self.RST_PIN = rst
self.DC_PIN = dc
self.BL_PIN = bl
self.SPEED =spi_freq
self.BL_freq=bl_freq
self.SPEED = spi_freq
self.BL_freq = bl_freq
self.GPIO = RPi.GPIO
#self.GPIO.cleanup()
# self.GPIO.cleanup()
self.GPIO.setmode(self.GPIO.BCM)
self.GPIO.setwarnings(False)
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BL_PIN, self.GPIO.OUT)
self.GPIO.output(self.BL_PIN, self.GPIO.HIGH)
#Initialize SPI
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BL_PIN, self.GPIO.OUT)
self.GPIO.output(self.BL_PIN, self.GPIO.HIGH)
# Initialize SPI
self.SPI = spidev.SpiDev()
self.SPI.open(0, 0)
self.SPI.max_speed_hz = spi_freq
@ -67,28 +76,29 @@ class RaspberryPi:
time.sleep(delaytime / 1000.0)
def spi_writebyte(self, data):
if self.SPI!=None :
if self.SPI != None:
self.SPI.writebytes(data)
def bl_DutyCycle(self, duty):
self._pwm.ChangeDutyCycle(duty)
def bl_Frequency(self,freq):
def bl_Frequency(self, freq):
self._pwm.ChangeFrequency(freq)
def module_init(self):
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BL_PIN, self.GPIO.OUT)
self._pwm=self.GPIO.PWM(self.BL_PIN,self.BL_freq)
self._pwm = self.GPIO.PWM(self.BL_PIN, self.BL_freq)
self._pwm.start(100)
if self.SPI!=None :
if self.SPI != None:
self.SPI.max_speed_hz = self.SPEED
self.SPI.mode = 0b00
return 0
def module_exit(self):
logging.debug("spi end")
if self.SPI!=None :
if self.SPI != None:
self.SPI.close()
logging.debug("gpio cleanup...")
@ -97,15 +107,15 @@ class RaspberryPi:
self._pwm.stop()
time.sleep(0.001)
self.GPIO.output(self.BL_PIN, 1)
#self.GPIO.cleanup()
# self.GPIO.cleanup()
'''
"""
if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
implementation = RaspberryPi()
for func in [x for x in dir(implementation) if not x.startswith('_')]:
setattr(sys.modules[__name__], func, getattr(implementation, func))
'''
"""
### END OF FILE ###

View File

@ -3,9 +3,9 @@ import lcdconfig
class LCD_2inch(lcdconfig.RaspberryPi):
width = 240
height = 320
def command(self, cmd):
self.digital_write(self.DC_PIN, self.GPIO.LOW)
self.spi_writebyte([cmd])
@ -13,13 +13,14 @@ class LCD_2inch(lcdconfig.RaspberryPi):
def data(self, val):
self.digital_write(self.DC_PIN, self.GPIO.HIGH)
self.spi_writebyte([val])
def reset(self):
"""Reset the display"""
self.GPIO.output(self.RST_PIN,self.GPIO.HIGH)
self.GPIO.output(self.RST_PIN, self.GPIO.HIGH)
time.sleep(0.01)
self.GPIO.output(self.RST_PIN,self.GPIO.LOW)
self.GPIO.output(self.RST_PIN, self.GPIO.LOW)
time.sleep(0.01)
self.GPIO.output(self.RST_PIN,self.GPIO.HIGH)
self.GPIO.output(self.RST_PIN, self.GPIO.HIGH)
time.sleep(0.01)
def Init(self):
@ -116,63 +117,74 @@ class LCD_2inch(lcdconfig.RaspberryPi):
self.command(0x29)
def SetWindows(self, Xstart, Ystart, Xend, Yend):
#set the X coordinates
# set the X coordinates
self.command(0x2A)
self.data(Xstart>>8) #Set the horizontal starting point to the high octet
self.data(Xstart & 0xff) #Set the horizontal starting point to the low octet
self.data(Xend>>8) #Set the horizontal end to the high octet
self.data((Xend - 1) & 0xff)#Set the horizontal end to the low octet
self.data(Xstart >> 8) # Set the horizontal starting point to the high octet
self.data(Xstart & 0xFF) # Set the horizontal starting point to the low octet
self.data(Xend >> 8) # Set the horizontal end to the high octet
self.data((Xend - 1) & 0xFF) # Set the horizontal end to the low octet
#set the Y coordinates
# set the Y coordinates
self.command(0x2B)
self.data(Ystart>>8)
self.data((Ystart & 0xff))
self.data(Yend>>8)
self.data((Yend - 1) & 0xff )
self.data(Ystart >> 8)
self.data((Ystart & 0xFF))
self.data(Yend >> 8)
self.data((Yend - 1) & 0xFF)
self.command(0x2C)
def ShowImage(self,Image,Xstart=0,Ystart=0):
def ShowImage(self, Image, Xstart=0, Ystart=0):
"""Set buffer to value of Python Imaging Library image."""
"""Write display buffer to physical display"""
imwidth, imheight = Image.size
if imwidth == self.height and imheight == self.width:
if imwidth == self.height and imheight == self.width:
img = self.np.asarray(Image)
pix = self.np.zeros((self.width, self.height,2), dtype = self.np.uint8)
#RGB888 >> RGB565
pix[...,[0]] = self.np.add(self.np.bitwise_and(img[...,[0]],0xF8),self.np.right_shift(img[...,[1]],5))
pix[...,[1]] = self.np.add(self.np.bitwise_and(self.np.left_shift(img[...,[1]],3),0xE0), self.np.right_shift(img[...,[2]],3))
pix = self.np.zeros((self.width, self.height, 2), dtype=self.np.uint8)
# RGB888 >> RGB565
pix[..., [0]] = self.np.add(
self.np.bitwise_and(img[..., [0]], 0xF8),
self.np.right_shift(img[..., [1]], 5),
)
pix[..., [1]] = self.np.add(
self.np.bitwise_and(self.np.left_shift(img[..., [1]], 3), 0xE0),
self.np.right_shift(img[..., [2]], 3),
)
pix = pix.flatten().tolist()
self.command(0x36)
self.data(0x70)
self.SetWindows ( 0, 0, self.height,self.width)
self.digital_write(self.DC_PIN,self.GPIO.HIGH)
for i in range(0,len(pix),4096):
self.spi_writebyte(pix[i:i+4096])
self.SetWindows(0, 0, self.height, self.width)
self.digital_write(self.DC_PIN, self.GPIO.HIGH)
for i in range(0, len(pix), 4096):
self.spi_writebyte(pix[i : i + 4096])
else :
else:
img = self.np.asarray(Image)
pix = self.np.zeros((imheight,imwidth , 2), dtype = self.np.uint8)
pix = self.np.zeros((imheight, imwidth, 2), dtype=self.np.uint8)
pix[...,[0]] = self.np.add(self.np.bitwise_and(img[...,[0]],0xF8),self.np.right_shift(img[...,[1]],5))
pix[...,[1]] = self.np.add(self.np.bitwise_and(self.np.left_shift(img[...,[1]],3),0xE0), self.np.right_shift(img[...,[2]],3))
pix[..., [0]] = self.np.add(
self.np.bitwise_and(img[..., [0]], 0xF8),
self.np.right_shift(img[..., [1]], 5),
)
pix[..., [1]] = self.np.add(
self.np.bitwise_and(self.np.left_shift(img[..., [1]], 3), 0xE0),
self.np.right_shift(img[..., [2]], 3),
)
pix = pix.flatten().tolist()
self.command(0x36)
self.data(0x00)
self.SetWindows ( 0, 0, self.width, self.height)
self.digital_write(self.DC_PIN,self.GPIO.HIGH)
for i in range(0,len(pix),4096):
self.spi_writebyte(pix[i:i+4096])
self.SetWindows(0, 0, self.width, self.height)
self.digital_write(self.DC_PIN, self.GPIO.HIGH)
for i in range(0, len(pix), 4096):
self.spi_writebyte(pix[i : i + 4096])
def clear(self):
"""Clear contents of image buffer"""
_buffer = [0xff]*(self.width * self.height * 2)
self.SetWindows ( 0, 0, self.height, self.width)
self.digital_write(self.DC_PIN,self.GPIO.HIGH)
for i in range(0,len(_buffer),4096):
self.spi_writebyte(_buffer[i:i+4096])
_buffer = [0xFF] * (self.width * self.height * 2)
self.SetWindows(0, 0, self.height, self.width)
self.digital_write(self.DC_PIN, self.GPIO.HIGH)
for i in range(0, len(_buffer), 4096):
self.spi_writebyte(_buffer[i : i + 4096])

View File

@ -27,7 +27,7 @@ def display_loop(api: Api, cfg: Dict):
d.clear()
bg = Image.new("RGB", (d.height, d.width), (0, 0, 0))
d.ShowImage(bg)
Font0 = ImageFont.truetype("Font/Font00.ttf", 14)
Font1 = ImageFont.truetype("Font/Font00.ttf", 18)
Font2 = ImageFont.truetype("Font/Font00.ttf", 36)
@ -45,7 +45,12 @@ def display_loop(api: Api, cfg: Dict):
not_playing_img.paste(spoti_logo, (0, 10))
not_playing_img.paste(qr, (120, 40))
draw = ImageDraw.Draw(not_playing_img)
draw.text((124, 10), "Connect to speakers", font=Font1, fill=cfg["color_theme"]["text"])
draw.text(
(124, 10),
"Connect to speakers",
font=Font1,
fill=cfg["color_theme"]["text"],
)
del spoti_logo, qr, draw
last_api_call = 0
@ -59,12 +64,12 @@ def display_loop(api: Api, cfg: Dict):
if time.time() - last_auth_refresh >= auth_interval:
auth_interval = api.refreshAuth() - 120
last_auth_refresh = time.time()
print(f'Refreshed auth at {datetime.now().strftime("%d-%m %H:%M:%S")}')
print(f"Refreshed auth at {datetime.now().strftime('%d-%m %H:%M:%S')}")
if time.time() - last_api_call >= cfg["api_interval"]:
data = api.getPlaying()
last_api_call = time.time()
if data == None:
warnings.warn("No data found")
d.ShowImage(error_img)
@ -73,45 +78,85 @@ def display_loop(api: Api, cfg: Dict):
draw = ImageDraw.Draw(not_playing_img)
current_time = datetime.now().time()
draw.rectangle([(00, 120), (119, 170)], fill=(0, 0, 0))
draw.text((20, 120), f"{current_time.hour}:{current_time.minute:02d}", font=Font2, angle=0)
draw.text(
(20, 120),
f"{current_time.hour}:{current_time.minute:02d}",
font=Font2,
angle=0,
)
if current_mode != 0:
current_mode = 0
print("Standby mode")
d.ShowImage(not_playing_img)
time.sleep(cfg["api_interval"])
elif type(data) == dict:
current_track = data["track"] + data["artists"][0] + data["album"]
if current_track != last_track:
print('updating track')
print("updating track")
last_track = current_track
img = None
try:
img = requests.get(data["img_url"], timeout=cfg["refresh_interval"]/2)
img = requests.get(
data["img_url"], timeout=cfg["refresh_interval"] / 2
)
img = Image.open(BytesIO(img.content))
except Exception as e:
warnings.warn(f"Failed to fetch album cover at: {data['img_url']}\n{e}")
warnings.warn(
f"Failed to fetch album cover at: {data['img_url']}\n{e}"
)
if img == None:
img = unavailable_img
img = img.resize((128, 128))
bg = Image.new("RGB", (320, 240), cfg["color_theme"]["background"])
bg.paste(img, (10, 30))
draw = ImageDraw.Draw(bg)
draw.text((150, 40), "\n".join(textwrap.wrap(", ".join(data["artists"]), width=16)), font=Font1, fill=cfg["color_theme"]["text"])
draw.text((10, 160), "\n".join(textwrap.wrap(data["track"], width=32)), font=Font1, fill=cfg["color_theme"]["text"])
draw.text(
(150, 40),
"\n".join(textwrap.wrap(", ".join(data["artists"]), width=16)),
font=Font1,
fill=cfg["color_theme"]["text"],
)
draw.text(
(10, 160),
"\n".join(textwrap.wrap(data["track"], width=32)),
font=Font1,
fill=cfg["color_theme"]["text"],
)
w, h = bg.size
w -= 92
progress_time = data["progress_ms"] + int((time.time() - last_api_call) * 1000)
progress= progress_time / data["duration_ms"]
progress_time = data["progress_ms"] + int(
(time.time() - last_api_call) * 1000
)
progress = progress_time / data["duration_ms"]
bar_width = int(w * progress)
draw.rectangle([(8, h - 22), (w + 2, h - 8)], outline=cfg["color_theme"]["bar_outline"])
draw.rectangle([(10, h - 20), (w, h - 10)], fill=cfg["color_theme"]["background"])
draw.rectangle([(10, h - 20), (bar_width, h - 10)], fill=cfg["color_theme"]["bar_inside"])
f_current_time = "{:02.0f}:{:02.0f}".format(*divmod(progress_time // 1000, 60))
f_total_time = "{:02d}:{:02d}".format(*divmod(data["duration_ms"]// 1000, 60))
draw.rectangle([(234, 215), (310, 235)], fill=cfg["color_theme"]["background"])
draw.text((235, 215), f"{f_current_time}/{f_total_time}", font=Font0, fill=cfg["color_theme"]["text"])
draw.rectangle(
[(8, h - 22), (w + 2, h - 8)],
outline=cfg["color_theme"]["bar_outline"],
)
draw.rectangle(
[(10, h - 20), (w, h - 10)], fill=cfg["color_theme"]["background"]
)
draw.rectangle(
[(10, h - 20), (bar_width, h - 10)],
fill=cfg["color_theme"]["bar_inside"],
)
f_current_time = "{:02.0f}:{:02.0f}".format(
*divmod(progress_time // 1000, 60)
)
f_total_time = "{:02d}:{:02d}".format(
*divmod(data["duration_ms"] // 1000, 60)
)
draw.rectangle(
[(234, 215), (310, 235)], fill=cfg["color_theme"]["background"]
)
draw.text(
(235, 215),
f"{f_current_time}/{f_total_time}",
font=Font0,
fill=cfg["color_theme"]["text"],
)
d.ShowImage(bg)
time.sleep(cfg["refresh_interval"])
@ -125,4 +170,3 @@ if __name__ == "__main__":
cfg = parse_config()
api = Api(cfg["refresh_token"], cfg["base_64"])
display_loop(api, cfg)