musicmouse/espmusicmouse/host_driver/main.py

213 lines
8.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python
2021-11-27 21:18:54 +01:00
import asyncio
import sys
2021-11-27 21:18:54 +01:00
import serial_asyncio
from led_cmds import (ColorRGBW, ColorHSV, EffectStaticConfig,
EffectRandomTwoColorInterpolationConfig, EffectAlexaSwipeConfig,
2021-12-20 20:41:50 +01:00
EffectSwipeAndChange, EffectReverseSwipe)
2021-12-19 13:46:45 +01:00
from host_driver import MusicMouseProtocol, RfidTokenRead, RotaryEncoderEvent, ButtonEvent, TouchButton, TouchButtonPress, TouchButtonRelease
2021-11-27 22:05:48 +01:00
from player import AudioPlayer
from glob import glob
2021-12-20 20:41:50 +01:00
from copy import deepcopy
2021-11-27 22:05:48 +01:00
import os
2021-12-24 00:12:43 +01:00
from hass_client import HomeAssistantClient
import argparse
from ruamel.yaml import YAML
import warnings
from pprint import pprint
2021-11-27 22:05:48 +01:00
yaml = YAML(typ='safe')
2021-11-27 21:18:54 +01:00
OFF_COLOR = ColorRGBW(0, 0, 0, 0)
2021-11-27 21:18:54 +01:00
2021-12-19 13:46:45 +01:00
def parse_color(color_str: str):
2021-12-19 13:46:45 +01:00
if isinstance(color_str, ColorRGBW):
return color_str
elif color_str.startswith("#"):
color_str = color_str.lstrip('#')
t = tuple(int(color_str[i:i + 2], 16) / 255 for i in (0, 2, 4))
return ColorRGBW(*t, 0)
elif color_str.startswith("w"):
color_str = color_str.lstrip("w")
return ColorRGBW(0, 0, 0, int(color_str, 16) / 255)
def load_config(config_path):
with open(os.path.join(config_path, "config.yml")) as cfg_file:
cfg = yaml.load(cfg_file)
for figure_name, figure_cfg in cfg["figures"].items():
figure_cfg["colors"] = [parse_color(c) for c in figure_cfg["colors"]]
if 'media_files' not in figure_cfg:
figure_cfg['media_files'] = sorted(glob(os.path.join(config_path, figure_name)))
return cfg
class MusicMouseState:
def __init__(self, protocol: MusicMouseProtocol):
self.current_figure: str = None
self.last_figure: str = None
self.current_mouse_led_effect = None
self.current_led_ring_effect = None
self.protocol: MusicMouseProtocol = protocol
self.button_led_brightness = None
def mouse_led_effect(self, effect_cfg):
self.current_mouse_led_effect = effect_cfg
self.protocol.mouse_led_effect(effect_cfg)
def led_ring_effect(self, effect_cfg):
self.current_led_ring_effect = effect_cfg
self.protocol.led_ring_effect(effect_cfg)
def button_leds(self, brightness):
assert 0 <= brightness <= 1
self.protocol.button_background_led_prev(brightness)
self.protocol.button_background_led_next(brightness)
self.button_led_brightness = brightness
def reset(self):
self.mouse_led_effect(EffectStaticConfig(OFF_COLOR))
self.led_ring_effect(EffectStaticConfig(OFF_COLOR))
def figure_placed(self, figure_state):
self.last_figure = self.current_figure
self.current_figure = figure_state
def figure_removed(self):
self.last_figure = self.current_figure
class Controller:
def __init__(self, protocol, cfg):
self.cfg = cfg
self.audio_player = AudioPlayer(cfg["general"]["alsa_device"])
self.mmstate = MusicMouseState(protocol)
protocol.register_message_callback(self.on_firmware_msg)
self.audio_player.on_playlist_end_callback = self._run_off_animation
self.playlists = {
fig: self.audio_player.create_playlist(fig_cfg['media_files'])
for fig, fig_cfg in cfg['figures'].items()
}
self._rfid_to_figure_name = {
bytes.fromhex(figure_cfg["id"]): figure_name
for figure_name, figure_cfg in cfg["figures"].items()
}
def handle_rfid_event(self, tagid):
if tagid == bytes.fromhex("0000000000"):
if self.audio_player.is_playing():
self._run_off_animation()
self.audio_player.pause()
self.mmstate.figure_removed()
elif tagid in self._rfid_to_figure_name:
figure = self._rfid_to_figure_name[tagid]
primary_color, secondary_color, *rest = self.cfg["figures"]["colors"]
self._start_animation(primary_color, secondary_color)
self.mmstate.button_leds(self.cfg["general"].get("button_leds_brightness", 0.5))
if figure in self.playlists:
self.audio_player.set_playlist(self.playlists[figure])
if self.mmstate.last_figure == figure:
self.audio_player.play()
else:
self.audio_player.play_from_start()
self.mmstate.figure_placed(figure)
else:
warnings.warn(f"Unknown figure/tag with id {tagid}")
def on_firmware_msg(self, _, message):
print("FW msg:", message)
if isinstance(message, RfidTokenRead):
self.handle_rfid_event(message.id)
elif isinstance(message, RotaryEncoderEvent):
volume_increment = self.cfg["general"].get("volume_increment", 2)
if message.direction == 2:
self.audio_player.change_volume(volume_increment)
elif message.direction == 1:
self.audio_player.change_volume(-volume_increment)
elif isinstance(message, ButtonEvent):
if message.button == "left" and message.event == "pressed":
self.audio_player.previous()
elif message.button == "right" and message.event == "pressed":
self.audio_player.next()
elif False and isinstance(message, TouchButtonPress):
if current_figure:
ccfg = color_cfg[current_figure]
protocol.mouse_led_effect(
EffectStaticConfig(ccfg.accent, *mouse_leds[message.touch_button]))
#if message.touch_button == TouchButton.RIGHT_FOOT:
# asyncio.create_task(
# hass.call_service("switch", "toggle", {"entity_id": "switch.tasmota06"}))
# asyncio.create_task(
# hass.call_service("light", "turn_on", {"entity_id": "light.arbeitszimmer_fluter"}))
#elif message.touch_button == TouchButton.LEFT_FOOT:
# asyncio.create_task(
# hass.call_service("light", "turn_off", {"entity_id": "light.arbeitszimmer_fluter"}))
elif False and isinstance(message, TouchButtonRelease):
if current_figure:
ccfg = color_cfg[current_figure]
eff_change = EffectRandomTwoColorInterpolationConfig()
eff_static = EffectStaticConfig(ColorRGBW(0, 0, 0, 0),
*mouse_leds[message.touch_button])
if audio_player.is_playing():
eff_static.color = ccfg.primary
protocol.mouse_led_effect(eff_static)
if audio_player.is_playing():
eff_change.color1 = ccfg.primary
eff_change.color2 = ccfg.secondary
eff_change.start_with_existing = True
protocol.mouse_led_effect(eff_change)
def _start_animation(self, primary_color, secondary_color):
2021-12-19 13:46:45 +01:00
ring_eff = EffectSwipeAndChange()
ring_eff.swipe.primary_color = primary_color
ring_eff.swipe.secondary_color = secondary_color
2021-12-24 00:12:43 +01:00
ring_eff.swipe.swipe_speed = 180
ring_eff.change.color1 = primary_color
ring_eff.change.color2 = secondary_color
self.mmstate.led_ring_effect(ring_eff)
2021-12-19 13:46:45 +01:00
2021-12-20 20:41:50 +01:00
mouse_eff = deepcopy(ring_eff)
mouse_eff.swipe.start_position = 6 / 45 * 360
2021-12-20 21:32:05 +01:00
mouse_eff.swipe.bell_curve_width_in_leds = 16
2021-12-24 00:12:43 +01:00
mouse_eff.swipe.swipe_speed = 180
self.mmstate.mouse_led_effect(mouse_eff)
2021-12-19 13:46:45 +01:00
def _run_off_animation(self):
ring_eff = EffectReverseSwipe()
self.mmstate.led_ring_effect(ring_eff)
2021-12-20 21:32:05 +01:00
mouse_eff = EffectReverseSwipe()
mouse_eff.startPosition = 6 / 45 * 360
self.mmstate.mouse_led_effect(mouse_eff)
2021-12-19 13:46:45 +01:00
self.mmstate.button_leds(0)
2021-12-19 13:46:45 +01:00
def main(config_path):
cfg = load_config(config_path)
loop = asyncio.get_event_loop()
coro = serial_asyncio.create_serial_connection(loop,
MusicMouseProtocol,
cfg["general"]["serial_port"],
baudrate=115200)
transport, protocol = loop.run_until_complete(coro)
controller = Controller(protocol, cfg)
return controller, loop
if __name__ == "__main__":
if len(sys.argv) == 2:
controller, loop = main(config_path=sys.argv[1])
loop.run_forever()
loop.close()
else:
print("Error: run with config file path as first argument")