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 rev: v0.9.0 # Use the desired version of ruff
hooks: hooks:
- id: ruff - id: ruff
files: spotiplayer_pi/ args: ["--ignore=E711,E721"]
# args: ["--fix"] # Automatically apply fixes where possible # args: ["--fix"] # Automatically apply fixes where possible
- id: ruff-format - id: ruff-format
files: spotiplayer_pi/ args: ["--ignore=E711,E721"]

View File

@ -1,4 +1,6 @@
import requests import requests
import warnings
class Api: class Api:
def __init__(self, refresh_token: str, base_64: str): def __init__(self, refresh_token: str, base_64: str):
@ -13,11 +15,13 @@ class Api:
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": self.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.access_token = req["access_token"]
self.header = {"Authorization": f"Bearer {self.access_token}"} self.header = {"Authorization": f"Bearer {self.access_token}"}
return req["expires_in"] return req["expires_in"]
def getPlaying(self): def getPlaying(self):
url = "https://api.spotify.com/v1/me/player/currently-playing" url = "https://api.spotify.com/v1/me/player/currently-playing"
req = requests.get(url, headers=self.header) req = requests.get(url, headers=self.header)
@ -42,8 +46,7 @@ class Api:
if img_urls: if img_urls:
res["img_url"] = img_urls.pop()["url"] res["img_url"] = img_urls.pop()["url"]
else: 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 return res

View File

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

View File

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

View File

@ -27,7 +27,7 @@ def display_loop(api: Api, cfg: Dict):
d.clear() d.clear()
bg = Image.new("RGB", (d.height, d.width), (0, 0, 0)) bg = Image.new("RGB", (d.height, d.width), (0, 0, 0))
d.ShowImage(bg) d.ShowImage(bg)
Font0 = ImageFont.truetype("Font/Font00.ttf", 14) Font0 = ImageFont.truetype("Font/Font00.ttf", 14)
Font1 = ImageFont.truetype("Font/Font00.ttf", 18) Font1 = ImageFont.truetype("Font/Font00.ttf", 18)
Font2 = ImageFont.truetype("Font/Font00.ttf", 36) 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(spoti_logo, (0, 10))
not_playing_img.paste(qr, (120, 40)) not_playing_img.paste(qr, (120, 40))
draw = ImageDraw.Draw(not_playing_img) 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 del spoti_logo, qr, draw
last_api_call = 0 last_api_call = 0
@ -59,12 +64,12 @@ def display_loop(api: Api, cfg: Dict):
if time.time() - last_auth_refresh >= auth_interval: if time.time() - last_auth_refresh >= auth_interval:
auth_interval = api.refreshAuth() - 120 auth_interval = api.refreshAuth() - 120
last_auth_refresh = time.time() 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"]: if time.time() - last_api_call >= cfg["api_interval"]:
data = api.getPlaying() data = api.getPlaying()
last_api_call = time.time() last_api_call = time.time()
if data == None: if data == None:
warnings.warn("No data found") warnings.warn("No data found")
d.ShowImage(error_img) d.ShowImage(error_img)
@ -73,45 +78,85 @@ def display_loop(api: Api, cfg: Dict):
draw = ImageDraw.Draw(not_playing_img) draw = ImageDraw.Draw(not_playing_img)
current_time = datetime.now().time() current_time = datetime.now().time()
draw.rectangle([(00, 120), (119, 170)], fill=(0, 0, 0)) 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: if current_mode != 0:
current_mode = 0 current_mode = 0
print("Standby mode") print("Standby mode")
d.ShowImage(not_playing_img) d.ShowImage(not_playing_img)
time.sleep(cfg["api_interval"]) time.sleep(cfg["api_interval"])
elif type(data) == dict: elif type(data) == dict:
current_track = data["track"] + data["artists"][0] + data["album"] current_track = data["track"] + data["artists"][0] + data["album"]
if current_track != last_track: if current_track != last_track:
print('updating track') print("updating track")
last_track = current_track last_track = current_track
img = None img = None
try: 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)) img = Image.open(BytesIO(img.content))
except Exception as e: 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: if img == None:
img = unavailable_img img = unavailable_img
img = img.resize((128, 128)) img = img.resize((128, 128))
bg = Image.new("RGB", (320, 240), cfg["color_theme"]["background"]) bg = Image.new("RGB", (320, 240), cfg["color_theme"]["background"])
bg.paste(img, (10, 30)) bg.paste(img, (10, 30))
draw = ImageDraw.Draw(bg) 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(
draw.text((10, 160), "\n".join(textwrap.wrap(data["track"], width=32)), font=Font1, fill=cfg["color_theme"]["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, h = bg.size
w -= 92 w -= 92
progress_time = data["progress_ms"] + int((time.time() - last_api_call) * 1000) progress_time = data["progress_ms"] + int(
progress= progress_time / data["duration_ms"] (time.time() - last_api_call) * 1000
)
progress = progress_time / data["duration_ms"]
bar_width = int(w * progress) bar_width = int(w * progress)
draw.rectangle([(8, h - 22), (w + 2, h - 8)], outline=cfg["color_theme"]["bar_outline"]) draw.rectangle(
draw.rectangle([(10, h - 20), (w, h - 10)], fill=cfg["color_theme"]["background"]) [(8, h - 22), (w + 2, h - 8)],
draw.rectangle([(10, h - 20), (bar_width, h - 10)], fill=cfg["color_theme"]["bar_inside"]) outline=cfg["color_theme"]["bar_outline"],
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(
draw.rectangle([(234, 215), (310, 235)], fill=cfg["color_theme"]["background"]) [(10, h - 20), (w, h - 10)], 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(
[(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) d.ShowImage(bg)
time.sleep(cfg["refresh_interval"]) time.sleep(cfg["refresh_interval"])
@ -125,4 +170,3 @@ if __name__ == "__main__":
cfg = parse_config() cfg = parse_config()
api = Api(cfg["refresh_token"], cfg["base_64"]) api = Api(cfg["refresh_token"], cfg["base_64"])
display_loop(api, cfg) display_loop(api, cfg)