diff --git a/.gitignore b/.gitignore index c2ea484..995e6e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ generated_3d +venv build *.FCStd1 -*.blend1 \ No newline at end of file +*.blend1 diff --git a/espmusicmouse/host_driver/host_driver.py b/espmusicmouse/host_driver/host_driver.py index d4c4c39..bffc7b8 100644 --- a/espmusicmouse/host_driver/host_driver.py +++ b/espmusicmouse/host_driver/host_driver.py @@ -39,6 +39,13 @@ mouse_led_effect_to_message_id = { EffectReverseSwipe: 10, } +mouse_leds_index_ranges = { + TouchButton.RIGHT_FOOT: (0, 6), + TouchButton.LEFT_FOOT: (6, 6 + 6), + TouchButton.LEFT_EAR: (6 + 6, 6 + 6 + 16), + TouchButton.RIGHT_EAR: (6 + 6 + 16, 6 + 6 + 16 + 17), +} + PREV_BUTTON_LED_MSG = 20 NEXT_BUTTON_LED_MSG = 21 diff --git a/espmusicmouse/host_driver/main.py b/espmusicmouse/host_driver/main.py old mode 100644 new mode 100755 index cf8e113..f9d015f --- a/espmusicmouse/host_driver/main.py +++ b/espmusicmouse/host_driver/main.py @@ -1,4 +1,7 @@ +#!/usr/bin/env python + import asyncio +import sys import serial_asyncio from led_cmds import (ColorRGBW, ColorHSV, EffectStaticConfig, EffectRandomTwoColorInterpolationConfig, EffectAlexaSwipeConfig, @@ -9,175 +12,201 @@ from glob import glob from copy import deepcopy import os from hass_client import HomeAssistantClient +import argparse +from ruamel.yaml import YAML +import warnings +from pprint import pprint -HASS_URL = "https://ha.bauer.tech" -HASS_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI3YmY1OTc3NWJhZGI0MzFkOTBiNDBkZDA3OGQ1MTMxNSIsImlhdCI6MTY0MDAzNzI3MSwiZXhwIjoxOTU1Mzk3MjcxfQ.c7RPRP_hxzIwd3xcFTNLz94rOjLQDR0elH8RE-jCDgc" -MUSIC_FOLDER = "/home/martin/code/musicmouse/espmusicmouse/host_driver/music" +yaml = YAML(typ='safe') -audio_player = AudioPlayer() -current_figure = None -last_figure = None - -rfid_token_map = { - bytes.fromhex("88041174e9"): "elefant", - bytes.fromhex("8804ce7230"): "fuchs", - bytes.fromhex("88040d71f0"): "eule", - bytes.fromhex("88043c6ede"): "omnom", - bytes.fromhex("88040b78ff"): "eichhoernchen", - bytes.fromhex("8804bc7444"): "hund", -} +OFF_COLOR = ColorRGBW(0, 0, 0, 0) -def parse_hex_color(color_str: str): +def parse_color(color_str: str): if isinstance(color_str, ColorRGBW): return color_str - 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("#"): + 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) -class FigureColorCfg: - def __init__(self, primary, secondary, background, accent): - self.primary = parse_hex_color(primary) - self.secondary = parse_hex_color(secondary) - self.background = parse_hex_color(background) - self.accent = parse_hex_color(accent) +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 -color_cfg = { - "elefant": FigureColorCfg("#ffff00", "#00c8ff", "#094b46", "#c20099"), - "fuchs": FigureColorCfg("#F4D35E", "#F95738", "#F95738", "#083d77"), - "omnom": FigureColorCfg("#005102", "#3bc405", "#005102", "#3bc405"), - "eichhoernchen": FigureColorCfg("#ff0ada", "#4BC6B9", "#69045a", "#4BC6B9"), - "hund": FigureColorCfg("#ffff00", "#00c8ff", "#094b46", "#c20099"), - "eule": FigureColorCfg("#e5a200", "#f8e300", ColorRGBW(0, 0, 0, 0.2), ColorRGBW(0, 0, 0, 1)), -} +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 -playlists = { - fig: audio_player.create_playlist(sorted(glob(os.path.join(MUSIC_FOLDER, fig, "*.mp3")))) - for fig in rfid_token_map.values() -} + def mouse_led_effect(self, effect_cfg): + self.current_mouse_led_effect = effect_cfg + self.protocol.mouse_led_effect(effect_cfg) -mouse_leds = { - TouchButton.RIGHT_FOOT: (0, 6), - TouchButton.LEFT_FOOT: (6, 6 + 6), - TouchButton.LEFT_EAR: (6 + 6, 6 + 6 + 16), - TouchButton.RIGHT_EAR: (6 + 6 + 16, 6 + 6 + 16 + 17), -} + 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 -def run_off_animation(protocol): - ring_eff = EffectReverseSwipe() - mouse_eff = EffectReverseSwipe() - mouse_eff.startPosition = 6 / 45 * 360 - protocol.led_ring_effect(ring_eff) - protocol.mouse_led_effect(mouse_eff) - protocol.button_background_led_prev(0) - protocol.button_background_led_next(0) +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) -def on_music_end_callback(protocol): - run_off_animation(protocol) + 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)) -def on_rfid(protocol, tagid): - global current_figure, last_figure + 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() - if tagid == bytes.fromhex("0000000000"): - # Off - if audio_player.is_playing(): - run_off_animation(protocol) - audio_player.pause() - #else: - # protocol.led_ring_effect(EffectStaticConfig(ColorRGBW(0, 0, 0, 0))) - # protocol.mouse_led_effect(EffectStaticConfig(ColorRGBW(0, 0, 0, 0))) - last_figure = current_figure - else: - figure = rfid_token_map[tagid] - current_figure = figure - ccfg = color_cfg[figure] + 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): ring_eff = EffectSwipeAndChange() - ring_eff.swipe.primary_color = ccfg.primary - ring_eff.swipe.secondary_color = ccfg.secondary + ring_eff.swipe.primary_color = primary_color + ring_eff.swipe.secondary_color = secondary_color ring_eff.swipe.swipe_speed = 180 - ring_eff.change.color1 = ccfg.primary - ring_eff.change.color2 = ccfg.secondary - protocol.led_ring_effect(ring_eff) + ring_eff.change.color1 = primary_color + ring_eff.change.color2 = secondary_color + self.mmstate.led_ring_effect(ring_eff) - mouse_eff = EffectStaticConfig(ccfg.background) mouse_eff = deepcopy(ring_eff) mouse_eff.swipe.start_position = 6 / 45 * 360 mouse_eff.swipe.bell_curve_width_in_leds = 16 mouse_eff.swipe.swipe_speed = 180 - protocol.mouse_led_effect(mouse_eff) + self.mmstate.mouse_led_effect(mouse_eff) - protocol.button_background_led_prev(0.3) - protocol.button_background_led_next(0.3) + def _run_off_animation(self): + ring_eff = EffectReverseSwipe() + self.mmstate.led_ring_effect(ring_eff) - if figure in playlists: - audio_player.set_playlist(playlists[figure]) - if last_figure == current_figure: - audio_player.play() - else: - audio_player.play_from_start() + mouse_eff = EffectReverseSwipe() + mouse_eff.startPosition = 6 / 45 * 360 + self.mmstate.mouse_led_effect(mouse_eff) + + self.mmstate.button_leds(0) -def on_firmware_msg(protocol: MusicMouseProtocol, message): - print("FW msg:", message) - if isinstance(message, RfidTokenRead): - on_rfid(protocol, message.id) - elif isinstance(message, RotaryEncoderEvent): - if audio_player.is_playing(): - if message.direction == 2: - audio_player.change_volume(2) - elif message.direction == 1: - audio_player.change_volume(-2) - elif isinstance(message, ButtonEvent): - if message.button == "left" and message.event == "pressed": - audio_player.previous() - elif message.button == "right" and message.event == "pressed": - 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 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 -loop = asyncio.get_event_loop() -hass = HomeAssistantClient(HASS_URL, HASS_TOKEN, loop) -coro = serial_asyncio.create_serial_connection(loop, - MusicMouseProtocol, - '/dev/ttyUSB0', - baudrate=115200) -transport, protocol = loop.run_until_complete(coro) -protocol.register_message_callback(on_firmware_msg) - -audio_player.on_playlist_end_callback = lambda: on_music_end_callback(protocol) - -loop.create_task(hass.connect()) -loop.run_forever() -loop.close() \ No newline at end of file +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") diff --git a/espmusicmouse/host_driver/player.py b/espmusicmouse/host_driver/player.py index 21d3108..20f9589 100644 --- a/espmusicmouse/host_driver/player.py +++ b/espmusicmouse/host_driver/player.py @@ -69,8 +69,9 @@ all_events = ( class AudioPlayer: - def __init__(self): - self.instance = vlc.Instance() + def __init__(self, alsa_device=None): + params = ["-A", "alsa", "--alsa-audio-device", alsa_device] if alsa_device else [] + self.instance = vlc.Instance(*params) self.media_list_player = self.instance.media_list_player_new() self.media_player = self.media_list_player.get_media_player()