initial commit
This commit is contained in:
commit
786b530dc1
127
.gitignore
vendored
Normal file
127
.gitignore
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
scratch.py
|
||||
service.json
|
||||
|
||||
*~*
|
||||
*#
|
||||
|
||||
node_modules/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
*/__pycache__/*
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
7
README.md
Normal file
7
README.md
Normal file
@ -0,0 +1,7 @@
|
||||
Ticker
|
||||
========
|
||||
|
||||
Interfacing with the GPIO pins on a Raspberry Pi.
|
||||
Using GPIOZero and RPLCD.
|
||||
|
||||
* Button, Buzzer, LED, LCD
|
36
main.py
Normal file
36
main.py
Normal file
@ -0,0 +1,36 @@
|
||||
import os
|
||||
from signal import pause
|
||||
|
||||
from ticker.ticker import Ticker
|
||||
|
||||
red_led_pin = 13
|
||||
yellow_led_pin = 19
|
||||
green_led_pin = 26
|
||||
|
||||
buzzer_pin = 7
|
||||
|
||||
button_pins = [21, 20, 16, 12]
|
||||
|
||||
lcd_rs = 18
|
||||
lcd_e = 15
|
||||
lcd_data = [14, 4, 3, 2]
|
||||
|
||||
|
||||
def main():
|
||||
tick = Ticker(lcd_rs=lcd_rs,
|
||||
lcd_e=lcd_e,
|
||||
lcd_data=lcd_data,
|
||||
buzzer_pin=buzzer_pin,
|
||||
red_led_pin=red_led_pin,
|
||||
yellow_led_pin=yellow_led_pin,
|
||||
green_led_pin=green_led_pin,
|
||||
button_pins=button_pins,
|
||||
|
||||
fm_username=os.environ.get('FM_USERNAME', 'sarsoo'),
|
||||
fm_key=os.environ['FM_KEY'])
|
||||
tick.start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
pause()
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
certifi==2020.6.20
|
||||
chardet==3.0.4
|
||||
colorzero==1.1
|
||||
gpiozero==1.5.1
|
||||
idna==2.10
|
||||
requests==2.24.0
|
||||
RPLCD==1.3.0
|
||||
urllib3==1.25.9
|
13
ticker.service
Normal file
13
ticker.service
Normal file
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Ticker
|
||||
|
||||
[Service]
|
||||
# Command to execute when the service is started
|
||||
ExecStart=/usr/bin/python3 /home/pi/ticker/main.py
|
||||
|
||||
[Service]
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Type=simple
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
16
ticker/__init__.py
Normal file
16
ticker/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
import logging
|
||||
from ticker.ticker import Ticker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
fmlogger = logging.getLogger('fmframework')
|
||||
logger.setLevel('DEBUG')
|
||||
|
||||
file_handler = logging.FileHandler("ticker.log")
|
||||
file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(name)s - %(funcName)s - %(message)s'))
|
||||
logger.addHandler(file_handler)
|
||||
fmlogger.addHandler(file_handler)
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(logging.Formatter('%(levelname)s %(name)s:%(funcName)s - %(message)s'))
|
||||
logger.addHandler(stream_handler)
|
||||
fmlogger.addHandler(stream_handler)
|
47
ticker/display/__init__.py
Normal file
47
ticker/display/__init__.py
Normal file
@ -0,0 +1,47 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DisplayItem:
|
||||
title: str = None
|
||||
message: str = ''
|
||||
wrap_line: bool = False
|
||||
iterations: int = 2
|
||||
time: int = 5
|
||||
|
||||
|
||||
def scroll_text(text, iterations=2, width=15):
|
||||
if len(text) < width:
|
||||
return text
|
||||
for iteration in range(iterations):
|
||||
start_idx = 0
|
||||
final_idx = width
|
||||
while final_idx <= len(text):
|
||||
yield text[start_idx:final_idx]
|
||||
start_idx += 1
|
||||
final_idx += 1
|
||||
|
||||
|
||||
def loop_text(text, width=15):
|
||||
while True:
|
||||
if text:
|
||||
for scrolled in scroll_text(text, iterations=1, width=width):
|
||||
yield scrolled
|
||||
yield ''
|
||||
else:
|
||||
yield ''
|
||||
|
||||
|
||||
def zip_lines(top_text='', bottom_text='', iterations=2, width=15):
|
||||
if len(top_text) == len(bottom_text):
|
||||
return zip(
|
||||
scroll_text(top_text, iterations, width), scroll_text(bottom_text, iterations, width)
|
||||
)
|
||||
elif len(top_text) > len(bottom_text):
|
||||
return zip(
|
||||
scroll_text(top_text, iterations, width), loop_text(bottom_text, width)
|
||||
)
|
||||
elif len(top_text) < len(bottom_text):
|
||||
return zip(
|
||||
loop_text(top_text, width), scroll_text(bottom_text, iterations, width)
|
||||
)
|
170
ticker/ticker.py
Normal file
170
ticker/ticker.py
Normal file
@ -0,0 +1,170 @@
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
import logging
|
||||
from datetime import date
|
||||
|
||||
from RPi import GPIO
|
||||
from RPLCD.gpio import CharLCD
|
||||
from gpiozero import TrafficLights, TonalBuzzer, Button
|
||||
from requests import Session
|
||||
|
||||
from fmframework.net.network import Network, LastFMNetworkException
|
||||
|
||||
from ticker.display import DisplayItem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
lcd_width = 16
|
||||
|
||||
|
||||
class Ticker:
|
||||
def __init__(self,
|
||||
lcd_rs,
|
||||
lcd_e,
|
||||
lcd_data,
|
||||
buzzer_pin,
|
||||
red_led_pin,
|
||||
yellow_led_pin,
|
||||
green_led_pin,
|
||||
button_pins,
|
||||
|
||||
fm_username: str,
|
||||
fm_key: str):
|
||||
self.lcd = CharLCD(numbering_mode=GPIO.BCM,
|
||||
cols=lcd_width,
|
||||
rows=2,
|
||||
pin_rs=lcd_rs,
|
||||
pin_e=lcd_e,
|
||||
pins_data=lcd_data,
|
||||
auto_linebreaks=True)
|
||||
self.leds = TrafficLights(red=red_led_pin, yellow=yellow_led_pin, green=green_led_pin, pwm=True)
|
||||
|
||||
self.buzzer = TonalBuzzer(buzzer_pin)
|
||||
|
||||
self.notif_button = Button(button_pins[0])
|
||||
self.notif_button.when_activated = self.handle_notif_click
|
||||
|
||||
self.button2 = Button(button_pins[1])
|
||||
self.button2.when_activated = lambda: self.queue_text('hey', 'hey')
|
||||
|
||||
self.button3 = Button(button_pins[2])
|
||||
self.button4 = Button(button_pins[3])
|
||||
|
||||
self.idle_text = dict()
|
||||
|
||||
self.notification_queue = Queue()
|
||||
self.display_queue = Queue()
|
||||
self.display_thread = Thread(target=self.display_worker, name='display', daemon=True)
|
||||
|
||||
self.rsession = Session()
|
||||
self.fmnet = Network(username=fm_username, api_key=fm_key)
|
||||
|
||||
self.puller_thread = Thread(target=self.puller_worker, name='puller', daemon=True)
|
||||
|
||||
def start(self):
|
||||
logger.info('starting ticker')
|
||||
|
||||
self.lcd.clear()
|
||||
self.display_thread.start()
|
||||
self.puller_thread.start()
|
||||
self.set_status(green=True)
|
||||
|
||||
def set_status(self,
|
||||
green: bool = False,
|
||||
yellow: bool = False,
|
||||
red: bool = False):
|
||||
self.leds.green.value = 1 if green else 0
|
||||
self.leds.yellow.value = 1 if yellow else 0
|
||||
self.leds.red.value = 1 if red else 0
|
||||
|
||||
def handle_notif_click(self):
|
||||
if not self.notification_queue.empty():
|
||||
while not self.notification_queue.empty():
|
||||
self.display_queue.put(self.notification_queue.get())
|
||||
self.leds.red.off()
|
||||
else:
|
||||
self.queue_text('No Notifications', '', interrupt=True, time=2)
|
||||
|
||||
def puller_worker(self):
|
||||
while True:
|
||||
|
||||
try:
|
||||
total = self.fmnet.get_scrobble_count_from_date(input_date=date.today())
|
||||
|
||||
logger.debug(f'loaded daily scrobbles {total}')
|
||||
|
||||
self.queue_text('Scrobbles Today', total)
|
||||
self.idle_text['daily_scrobbles'] = DisplayItem('Scrobbles', str(total))
|
||||
except LastFMNetworkException as e:
|
||||
logger.exception(e)
|
||||
self.queue_text('Last.FM Error', f'{e.http_code}, {e.error_code}, {e.message}')
|
||||
|
||||
sleep(30)
|
||||
|
||||
def display_worker(self):
|
||||
while True:
|
||||
if not self.display_queue.empty():
|
||||
display_item = self.display_queue.get(block=False)
|
||||
logger.info(f'dequeued {display_item}, size {self.display_queue.qsize()}')
|
||||
|
||||
self.write_display_item(display_item)
|
||||
|
||||
self.display_queue.task_done()
|
||||
|
||||
else:
|
||||
if len(self.idle_text) > 0:
|
||||
for key, item in self.idle_text.items():
|
||||
if self.display_queue.empty():
|
||||
break
|
||||
|
||||
logger.debug(f'writing {key}')
|
||||
self.write_display_item(item)
|
||||
|
||||
else:
|
||||
self.write_to_lcd(['Ticker...', ''])
|
||||
sleep(0.1)
|
||||
|
||||
def write_display_item(self, display_item):
|
||||
"""write display item to LCD now"""
|
||||
if display_item.message is None:
|
||||
display_item.message = ''
|
||||
|
||||
if len(display_item.message) > lcd_width:
|
||||
buffer = [display_item.title, '']
|
||||
self.write_to_lcd(buffer)
|
||||
self.loop_string(display_item.message, buffer, row=1, iterations=display_item.iterations)
|
||||
else:
|
||||
buffer = [display_item.title, display_item.message]
|
||||
self.write_to_lcd(buffer)
|
||||
sleep(display_item.time)
|
||||
|
||||
def queue_notification(self, title, message, wrap_line=False, iterations=2):
|
||||
logger.debug(f'queueing {title}/{message} {iterations} times, wrapped: {wrap_line}')
|
||||
self.notification_queue.put(DisplayItem(title, str(message), wrap_line, iterations))
|
||||
self.leds.red.pulse()
|
||||
|
||||
def queue_text(self, title, message, wrap_line=False, time=5, iterations=2, interrupt=False):
|
||||
logger.debug(f'queueing {title}/{message} {iterations} times, wrapped: {wrap_line}')
|
||||
|
||||
item = DisplayItem(title, str(message), wrap_line=wrap_line, iterations=iterations, time=time)
|
||||
if interrupt:
|
||||
self.write_display_item(item)
|
||||
else:
|
||||
self.display_queue.put(item)
|
||||
|
||||
def write_to_lcd(self, framebuffer):
|
||||
"""Write the framebuffer out to the specified LCD."""
|
||||
self.lcd.home()
|
||||
for row in framebuffer:
|
||||
self.lcd.write_string(row.ljust(lcd_width)[:lcd_width])
|
||||
self.lcd.write_string('\r\n')
|
||||
|
||||
def loop_string(self, string, framebuffer, row, delay=0.4, iterations=2):
|
||||
padding = ' ' * lcd_width
|
||||
s = padding + string + padding
|
||||
for round_trip in range(iterations):
|
||||
for i in range(len(s) - lcd_width + 1):
|
||||
framebuffer[row] = s[i:i + lcd_width]
|
||||
self.write_to_lcd(framebuffer)
|
||||
sleep(delay)
|
Loading…
Reference in New Issue
Block a user