📚

Pythonで簡易的なOSC通信用のCLIツールを作った

2024/12/16に公開

はじめに

どうも、でんのこです。
先日とあるきっかけでPCのキーボード入力をトリガーとしてOSC通信を行うCLIツールを作成したので軽く紹介します。
※普段Pythonはほぼ書かないので書き方の良し悪しはわかりません。

概要

やりたいこと

予め決められた内容のメッセージをキーボードショートカットですぐに送信できるようにすること。

機能

  • 送信先のIPアドレスの設定
  • 送信先のポートを設定
  • OSCの宛先を設定
  • 送信する値の設定(今回はInt, Float, Boolに対応)
  • 送信するためのキーボードショートカットを設定
  • 設定の保存・削除

実装

プログラム全体

import json
import os
from typing import List, Dict, Any
import sys
import pynput
import pythonosc
from pythonosc.udp_client import SimpleUDPClient
import keyboard
import argparse
import configparser

class OSCMessageSender:
    def __init__(self, config_path='osc_config.ini'):
        self.config_path = config_path
        self.destinations: List[Dict[str, Any]] = []
        self.load_config()

    def load_config(self):
        """設定ファイルから宛先とメッセージを読み込む"""
        config = configparser.ConfigParser()
        if os.path.exists(self.config_path):
            config.read(self.config_path)
            for section in config.sections():
                destination = {
                    'ip': config[section]['ip'],
                    'port': int(config[section]['port']),
                    'address': config[section]['address'],
                    'value': self._parse_value(config[section]['value']),
                    'shortcut': config[section]['shortcut']
                }
                self.destinations.append(destination)

    def _parse_value(self, value: str):
        """文字列値を適切な型に変換"""
        value = value.strip().lower()
        if value == 'true':
            return True
        elif value == 'false':
            return False
        elif value.isdigit():
            return int(value)
        elif value.replace('.', '', 1).isdigit():
            return float(value)
        return value

    def save_config(self):
        """設定ファイルに宛先とメッセージを保存"""
        config = configparser.ConfigParser()
        for i, dest in enumerate(self.destinations, 1):
            section = f'Destination{i}'
            config[section] = {
                'ip': dest['ip'],
                'port': str(dest['port']),
                'address': dest['address'],
                'value': str(dest['value']),  # すべての値を文字列に変換して保存
                'shortcut': dest['shortcut']
            }
        
        with open(self.config_path, 'w') as configfile:
            config.write(configfile)

    def add_destination(self, ip: str, port: int, address: str, value: Any, shortcut: str):
        """新しい宛先とメッセージを追加"""
        destination = {
            'ip': ip,
            'port': port,
            'address': address,
            'value': value,
            'shortcut': shortcut
        }
        self.destinations.append(destination)
        self.save_config()
        self.setup_shortcuts()

    def remove_destination(self, index: int):
        """指定したインデックスの宛先を削除"""
        if 0 <= index < len(self.destinations):
            del self.destinations[index]
            self.save_config()
            self.setup_shortcuts()
            print(f"Destination {index + 1} removed successfully.")
        else:
            print("Invalid destination index.")

    def send_osc_message(self, destination):
        """OSCメッセージを送信"""
        try:
            client = SimpleUDPClient(destination['ip'], destination['port'])
            client.send_message(destination['address'], destination['value'])
            print(f"Message sent to {destination['ip']}:{destination['port']}")
            print(f"Address: {destination['address']}, Value: {destination['value']} ({type(destination['value']).__name__})")
        except Exception as e:
            print(f"Error sending OSC message: {e}")

    def setup_shortcuts(self):
        """すべての宛先のキーボードショートカットを設定"""
        # 既存のショートカットをクリア
        keyboard.unhook_all()

        # 新しいショートカットを設定
        for dest in self.destinations:
            keyboard.add_hotkey(dest['shortcut'], self.send_osc_message, args=(dest,))

    def list_destinations(self):
        """現在の宛先とメッセージを表示"""
        for i, dest in enumerate(self.destinations, 1):
            print(f"Destination {i}:")
            print(f"  IP: {dest['ip']}")
            print(f"  Port: {dest['port']}")
            print(f"  Address: {dest['address']}")
            print(f"  Value: {dest['value']} ({type(dest['value']).__name__})")
            print(f"  Shortcut: {dest['shortcut']}")
            print()

def main():
    parser = argparse.ArgumentParser(description='OSC Message Sender')
    parser.add_argument('--add', action='store_true', help='Add a new destination')
    parser.add_argument('--list', action='store_true', help='List current destinations')
    parser.add_argument('--remove', type=int, help='Remove a destination by index')
    
    args = parser.parse_args()

    sender = OSCMessageSender()

    if args.remove is not None:
        sender.remove_destination(args.remove - 1)  # インデックスは0から始まるため調整
        return

    if args.add:
        ip = input("Enter destination IP: ")
        port = int(input("Enter destination port: "))
        address = input("Enter OSC address: ")
        
        # 値の入力と型変換
        while True:
            value_input = input("Enter value (true/false/number/string): ")
            try:
                value = sender._parse_value(value_input)
                break
            except ValueError:
                print("Invalid input. Please try again.")
        
        shortcut = input("Enter keyboard shortcut (e.g., 'ctrl+shift+a'): ")
        
        sender.add_destination(ip, port, address, value, shortcut)
        print("Destination added successfully.")

    if args.list:
        sender.list_destinations()

    if not args.add and not args.list and not args.remove:
        sender.setup_shortcuts()
        print("OSC Message Sender is running. Press Ctrl+C to exit.")
        keyboard.wait('esc')  # プログラムを終了するためのエスケープキー

if __name__ == '__main__':
    main()

以下に、プログラムをコードの塊ごとに概要としてまとめました。それぞれのセクションがプログラム全体でどのような役割を果たしているかを記載しています。

プログラムの概要

各関数の役割などを軽く紹介します。実装の詳細は割愛。

設定ファイルの管理

    def load_config(self):
        ...
    def save_config(self):
        ...
    def _parse_value(self, value: str):
        ...
  • load_config は設定ファイルを読み込んで、送信先リストを構築する。
  • save_config は送信先の設定をファイルに保存する。
  • _parse_value は値の型を文字列から適切な型(数値、真偽値など)に変換する。

宛先の管理

    def add_destination(self, ip: str, port: int, address: str, value: Any, shortcut: str):
        ...
    def remove_destination(self, index: int):
        ...
    def list_destinations(self):
        ...
  • 宛先の追加、削除、一覧表示を行う関数。

OSCメッセージ送信とショートカット設定

    def send_osc_message(self, destination):
        ...
    def setup_shortcuts(self):
        ...

概要:

  • send_osc_message は指定された宛先にOSCメッセージを送信する関数
  • setup_shortcuts はキーボードショートカットを設定し、キー入力によるメッセージ送信ができるようにする

メイン関数

def main():
    ...

コマンドライン引数を処理し、add(宛先の追加)、list(一覧表示)、remove(削除)の各操作を選択できる。引数がない場合はショートカット機能を有効にしプログラムがバックグラウンドで動作する。
これにより、このプログラムの実行中は他のソフトウェアを使用していても任意のタイミングでキーボードショートカットによるOSC通信を行うことが可能(便利)。

使い方

script_name.py はプログラムのファイル名です。

設定の追加

python script_name.py --add

画面に従ってCLIでIP, Port, OSC Address, Value (Int or Float or Bool), Shourtcut (ex, ctrl+shift+a) を順に入力する

OSC通信

設定を追加した状態で

python script_name.py

プログラムの実行中はキーボードショートカットによって設定したOSC通信が可能です。

設定の確認

python script_name.py --list

現在の設定の一覧が表示されます。

設定の削除

--listで設定を見て、削除したい設定が何番目のものか確認後以下のようにする。
ex) 3番めの設定を削除したい場合

python script_name.py --remove 3

#おわり
読んで頂きありがとうございました。

Discussion