Skip to content

Instantly share code, notes, and snippets.

@brouberol
Created September 15, 2022 08:30
Show Gist options
  • Save brouberol/f6b784cd08dc578760adec7cf7722393 to your computer and use it in GitHub Desktop.
Save brouberol/f6b784cd08dc578760adec7cf7722393 to your computer and use it in GitHub Desktop.
D&D Ambiance Keypad
import time
import math
from pimoroni_rgbkeypad import RGBKeypad
# From red to violet
## Row1
DARK_RED = (220, 20, 60)
RED = (255, 0, 0)
ORANGE_RED = (255, 69, 0)
ORANGE = (255, 165, 0)
## Row 2
GOLD = (255, 215, 0)
# YELLOW = (255, 255, 0)
GREEN_YELLOW = (173, 255, 47)
GREEN = (0, 255, 0)
LIME_GREEN = (50, 205, 50)
## Row 3
AQUAMARINE = (102, 205, 170)
DARK_CYAN = (32, 178, 170)
TURQUOISE = (0, 206, 209)
SKY_BLUE = (0, 191, 255)
## Row4
DARK_BLUE = (65, 105, 225)
BLUE_VIOLET = (138, 43, 226)
PURPLE = (186, 85, 211)
DEEP_PINK = (255, 20, 147)
COLORS = [
DARK_RED,
RED,
ORANGE_RED,
ORANGE,
GOLD,
GREEN_YELLOW,
GREEN,
LIME_GREEN,
AQUAMARINE,
DARK_CYAN,
TURQUOISE,
SKY_BLUE,
DARK_BLUE,
BLUE_VIOLET,
PURPLE,
DEEP_PINK,
]
PAUSE_KEY_INDEX = 15
ACTIVATED_KEY_BRIGHTNESS = 0.5
DEACTIVATED_KEY_BRIGHTNESS = 0.05
BRIGHTNESS_FLUCTUATION_CYCLE_MS = 3000
def fluctuating_brightness(t, cycle):
brightness = abs(math.cos(math.pi * t / cycle))
return flatten(value=brightness, min_value=0.05, max_value=0.80)
def flatten(value, min_value, max_value):
if value < min_value:
return min_value
elif value > max_value:
return max_value
return value
def initialize_keys(keypad):
for i, key in enumerate(keypad.keys):
key.color = COLORS[i]
key.brightness = DEACTIVATED_KEY_BRIGHTNESS
def main():
start_time = time.monotonic()
activated_keys = {}
keys_being_pressed = {}
keypad = RGBKeypad()
initialize_keys(keypad)
while True:
# This is faster than iterating over all the keys everytime
# BUT while the operator presses on the key, the key will be
# marked as pressed multiple times. We need to keep track of
# the keys that are _being_ pressed and only light them up once,
# to avoid a flicker effect
keys_pressed = keypad.get_keys_pressed()
for key_index, key_pressed in enumerate(keys_pressed):
# De-register a key that was being pressed if their state indicates
# that they are not being pressed.
if not key_pressed:
if key_index in keys_being_pressed:
keys_being_pressed.pop(key_index)
# When a key was activated, make its brightness fluctuate,
# except if the pause button is activated itself. In that case,
# only make the pause button fluctuate and deactivate all other
# activated keys, while keeping their activated state, to make it
# easy to restore
if PAUSE_KEY_INDEX in activated_keys and key_index != PAUSE_KEY_INDEX:
key = keypad.keys[key_index]
key.brightness = DEACTIVATED_KEY_BRIGHTNESS
elif key_index in activated_keys:
elapsed_ms = (time.monotonic() - start_time) * 1000
key = keypad.keys[key_index]
key.brightness = fluctuating_brightness(
elapsed_ms, cycle=BRIGHTNESS_FLUCTUATION_CYCLE_MS
)
continue
# Don't modify a key that is still being pressed, to avoid making it flicker
if key_index in keys_being_pressed:
continue
# When a key was pressed, send the associated keyboard event from the
# keypad to the computed it is connected to
key = keypad.keys[key_index]
keys_being_pressed[key_index] = True
# keyboard.send(KEY_INDEX_TO_KEYBOARD_KEY[key_index])
# Toggle the key activation state after it was pressed
if key_index in activated_keys:
activated_keys.pop(key_index)
key.brightness = DEACTIVATED_KEY_BRIGHTNESS
state = "off"
else:
activated_keys[key_index] = True
key.brightness = ACTIVATED_KEY_BRIGHTNESS
state = "on"
message = '{"key": "%s", "state": "%s"}\n' % (str(key_index), state)
print(message) # That sends the message over the usb port
if __name__ == "__main__":
main()
import contextlib
with contextlib.redirect_stdout(None):
import pygame
pygame.init()
import json
import sys
from tqdm import tqdm as progress
from pygame import mixer
from serial import Serial
from serial.tools.list_ports import grep as list_ports
PAUSE_KEY = "15"
paused = False
config = json.load(open("config.json"))
cached_sounds = {}
progress_bar = progress(config.items())
for key_name, sound_path in progress_bar:
progress_bar.set_description(f"Processing {sound_path}")
cached_sounds[key_name] = mixer.Sound(sound_path)
def process_message(message):
global paused
key_name = message["key"]
if key_name == PAUSE_KEY:
if not paused:
paused = True
pygame.mixer.pause()
else:
paused = False
pygame.mixer.unpause()
elif key_name in config:
key_channel = int(key_name)
try:
channel = mixer.Channel(key_channel)
except IndexError:
return
if message["state"] == "on":
sound_path = config.get(key_name)
if not sound_path:
return
channel.play(cached_sounds[key_name], loops=-1)
else:
channel.stop()
def main():
mixer.set_num_channels(len(config.keys()))
usb_ports = list(list_ports(r"^/dev/cu\.usbmodem.*$"))
if not usb_ports:
print("No USB-plugged keypad was found")
sys.exit(1)
usb_device = Serial(usb_ports[0].device)
while True:
line = usb_device.readline().strip()
line = line.decode("utf-8")
if not line.startswith("{"):
continue
try:
message = json.loads(line)
except ValueError:
continue
else:
process_message(message)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment