Skip to content

Commit

Permalink
backend basis
Browse files Browse the repository at this point in the history
  • Loading branch information
lpewewq committed Sep 30, 2022
1 parent 6a61ffb commit 8e77b86
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 24 deletions.
15 changes: 2 additions & 13 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
# Build artifacts
bin/

# Visual Studio configuration files
.vscode

# Valgrind files
vgcore*
massif*
callgrind*

# Spotify credentials
auth.sh

# Python files
__pycache__/
__pycache__
.cache
venv
instance
.env
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# Spotilights
# spotilights
13 changes: 13 additions & 0 deletions api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles

from .routers import spotify, strip

app = FastAPI()

api_router = APIRouter(prefix="/api")
api_router.include_router(spotify.router)
api_router.include_router(strip.router)

app.include_router(api_router)
# app.mount("/", StaticFiles(directory="svelte/public", html=True))
22 changes: 22 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pydantic import BaseSettings


class Settings(BaseSettings):
# LED strip
led_count: int # Number of LED pixels.
led_pin: int = 18 # GPIO pin connected to the pixels (18 uses PWM!, 10 uses SPI /dev/spidev0.0).
led_freq_hz: int = 800000 # LED signal frequency in hertz (usually 800khz)
led_dma: int = 10 # DMA channel to use for generating signal (try 10)
led_brightness: int = 64 # Set to 0 for darkest and 255 for brightest
led_invert: bool = False # True to invert the signal (when using NPN transistor level shift)
led_channel: int = 0 # set to '1' for GPIOs 13, 19, 41, 45 or 53
# Spotify
spotify_client_id: str
spotify_redirect_uri: str
spotify_scope: str = "user-read-playback-state"

class Config:
env_file = ".env"


settings = Settings()
Empty file added api/routers/__init__.py
Empty file.
33 changes: 33 additions & 0 deletions api/routers/spotify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import random

from fastapi import APIRouter
from fastapi.exceptions import HTTPException
from fastapi.responses import RedirectResponse

from ..config import settings
from ..spotify import get_spotify, spotify_auth_manager

router = APIRouter(prefix="/spotify")


@router.get("/me")
def index():
if spotify_auth_manager.get_cached_token() is None:
return None

return get_spotify().me()


@router.get("/oauth")
def oauth():
return spotify_auth_manager.get_authorize_url()


@router.get("/oauth-callback")
def callback(code: str, state: int):
if state != spotify_auth_manager.state:
raise HTTPException(status_code=401)

spotify_auth_manager.state = random.randint(1, 10e10)
spotify_auth_manager.get_access_token(code, check_cache=False)
return RedirectResponse("/")
66 changes: 66 additions & 0 deletions api/routers/strip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from fastapi import APIRouter, Query
from pydantic import BaseModel, condecimal
from rpi_ws281x import Color

from ..scheduler import to_byte, fill, rainbow, pride, theater
from ..strip import strip


class ColorModel(BaseModel):
red: condecimal(ge=0.0, le=1.0)
green: condecimal(ge=0.0, le=1.0)
blue: condecimal(ge=0.0, le=1.0)

def get_color(self):
r = to_byte(self.red)
g = to_byte(self.green)
b = to_byte(self.blue)
return Color(r, g, b)


router = APIRouter(prefix="/strip")


@router.on_event("shutdown")
def shutdown():
strip.fillColor(Color(0, 0, 0))
strip.show()


@router.get("/num-pixels")
def num_pixels():
return strip.numPixels()


@router.get("/pixels")
def get_pixels():
return strip.getPixels()[:]


@router.get("/brightness")
def get_brightness():
return strip.getBrightness()


@router.post("/brightness")
def set_brightness(brightness: float = Query(ge=0.0, le=1.0)):
strip.setBrightness(to_byte(brightness))


@router.post("/fill")
async def start_fill(color_model: ColorModel):
strip.start_animation(fill, color_model.get_color())


@router.post("/rainbow")
async def start_rainbow(delay: float = Query(0.5, ge=0.0)):
strip.start_animation(rainbow, delay)


@router.post("/pride")
async def start_pride():
strip.start_animation(pride)

@router.post("/theater")
async def start_theater(delay: float = Query(0.05, ge=0.0)):
strip.start_animation(theater, delay)
103 changes: 103 additions & 0 deletions api/scheduler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import asyncio
import ctypes as ct
import time
from colorsys import hsv_to_rgb

import numpy as np
from rpi_ws281x import Color


def to_byte(value: float, lower=0.0, upper=1.0):
res = min(value, upper)
res = max(lower, res)
return int(res * 255)


def wheel(pos):
"""Generate rainbow colors across 0-255 positions."""
if pos < 85:
return Color(pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return Color(255 - pos * 3, 0, pos * 3)
else:
pos -= 170
return Color(0, pos * 3, 255 - pos * 3)


async def rainbow(strip, delay):
while True:
for j in range(256):
for i in range(strip.numPixels()):
strip.setPixelColor(i, wheel((int(i * 256 / strip.numPixels()) + j) & 255))
strip.show()
await asyncio.sleep(delay)


async def theater(strip, delay):
while True:
for j in range(256):
for q in range(3):
for i in range(0, strip.numPixels(), 3):
strip.setPixelColor(i + q, wheel((i + j) % 255))
strip.show()
await asyncio.sleep(delay)
for i in range(0, strip.numPixels(), 3):
strip.setPixelColor(i + q, 0)


async def fill(strip, color):
while True:
strip.fillColor(color)
strip.show()
await asyncio.sleep(1)


async def pride(strip):
# Adapted from https://github.com/FastLED/FastLED/tree/b5874b588ade1d2639925e4e9719fa7d3c9d9e94/examples/Pride2015

def beatsin88(bpm, lowest, highest):
beat = time.time() * np.pi * bpm / 7680
beatsin = (np.sin(beat) + 1) / 2
rangewidth = highest - lowest
return int(lowest + rangewidth * beatsin)

sPseudotime = ct.c_uint16(0)
sLastMillis = time.time() * 1000
sHue16 = ct.c_uint16(0)

while True:
sat8 = ct.c_uint8(beatsin88(87, 220, 250))
brightdepth = ct.c_uint8(beatsin88(341, 96, 224))
brightnessthetainc16 = ct.c_uint16(beatsin88(203, 6400, 10240))
msmultiplier = ct.c_uint8(beatsin88(147, 23, 60))

hue16 = ct.c_uint16(sHue16.value)
hueinc16 = ct.c_uint16(beatsin88(113, 1, 3000))

ms = time.time() * 1000
deltams = ct.c_uint16(int(ms - sLastMillis))
sLastMillis = ms
sPseudotime = ct.c_uint16(sPseudotime.value + deltams.value * msmultiplier.value)
sHue16 = ct.c_uint16(sHue16.value + deltams.value * beatsin88(400, 5, 9))
brightnesstheta16 = ct.c_uint16(sPseudotime.value)

for i in range(strip.numPixels()):
hue16 = ct.c_uint16(hue16.value + hueinc16.value)
hue8 = ct.c_uint8(hue16.value // 256)

brightnesstheta16 = ct.c_uint16(brightnesstheta16.value + brightnessthetainc16.value)
b16 = ct.c_uint16(int((np.sin(np.pi * (brightnesstheta16.value / 32768)) + 1) * 32768))

bri16 = ct.c_uint16((b16.value * b16.value) // 65536)
bri8 = ct.c_uint8((bri16.value * brightdepth.value) // 65536)
bri8 = ct.c_uint8(bri8.value + 255 - brightdepth.value)
r, g, b = hsv_to_rgb(hue8.value / 255, sat8.value / 255, bri8.value / 255)

color = strip.getPixelColorRGB(i)
r = (3 * r + color.r / 255) / 4
g = (3 * g + color.g / 255) / 4
b = (3 * b + color.b / 255) / 4
strip.setPixelColorRGB(i, to_byte(r), to_byte(g), to_byte(b))
strip.show()
await asyncio.sleep(0)
17 changes: 17 additions & 0 deletions api/spotify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import random

from spotipy import Spotify, SpotifyPKCE

from .config import settings

spotify_auth_manager = SpotifyPKCE(
client_id=settings.spotify_client_id,
redirect_uri=settings.spotify_redirect_uri,
scope=settings.spotify_scope,
state=random.randint(1, 10e10),
open_browser=False,
)


def get_spotify():
return Spotify(auth_manager=spotify_auth_manager)
33 changes: 33 additions & 0 deletions api/strip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from rpi_ws281x import Color, PixelStrip
import asyncio

from .config import settings


class LEDStrip(PixelStrip):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.begin()
self.animation_task = None
self.fillColor(Color(0, 0, 0))
self.show()

def start_animation(self, animation_function, *args):
if self.animation_task is not None and not self.animation_task.done():
self.animation_task.cancel()
self.animation_task = asyncio.create_task(animation_function(self, *args))

def fillColor(self, color):
for i in range(self.numPixels()):
self.setPixelColor(i, color)


strip = LEDStrip(
num=settings.led_count,
pin=settings.led_pin,
freq_hz=settings.led_freq_hz,
dma=settings.led_dma,
invert=settings.led_invert,
brightness=settings.led_brightness,
channel=settings.led_channel,
)
10 changes: 0 additions & 10 deletions config_template.py

This file was deleted.

0 comments on commit 8e77b86

Please sign in to comment.