PythonでDUALSHOCK 4と通信する(Node-RED)
はじめに
今回はPythonでPS4のコントローラであるDUALSHOCK 4と通信してみました。
私が開発したpython-venvノードを利用すると、Node-REDでPythonのコードを実行できます。DUALSHOCK 4と通信して、さらにdashboardノードにつなげて状態を表示できるようにしてみました。
▼python-venvノードについては、Qiitaに開発の変遷を書きました。
https://qiita.com/background/items/d2e05e8d85427761a609
ロボットを操作できるようにしていきたいところです。
▼私は純正品のコントローラを利用しています。
▼以前の記事はこちら
環境を構築する
Node-REDとPythonを実行できる環境を構築します。
▼Node-REDのインストールについてはこちら
https://nodered.org/docs/getting-started/local
▼dashboardノードについてはこちら。Node-REDのインストール後、パレットの管理からインストールできます。
https://flows.nodered.org/node/node-red-dashboard
Pythonは事前にインストールしておいてください。
▼python-venvノードはこちら。こちらもパレットの管理からインストールできます。
https://flows.nodered.org/node/@background404/node-red-contrib-python-venv
pygameで受信してみる
コードはChatGPTに聞いてみました。いくつかのパッケージが提案されたのですが、今回は異なるOSでも利用できそうなpygameを使いました。
▼Node-REDでpipノードとvenvノードを配置しました。
▼pipノードではpygameをインストールするように設定して、実行しました。
まずは以下のコードを試してみました。
import pygame
# 初期化
pygame.init()
pygame.joystick.init()
# 接続されているコントローラーを取得
if pygame.joystick.get_count() == 0:
print("ジョイスティックが接続されていません")
exit()
joystick = pygame.joystick.Joystick(0)
joystick.init()
print(f"コントローラー: {joystick.get_name()} が接続されました")
# イベントループ
running = True
while running:
pygame.event.pump() # イベントを更新
# ボタンの状態を取得
for i in range(joystick.get_numbuttons()):
if joystick.get_button(i):
print(f"ボタン {i} が押されました")
# 軸の状態を取得
for i in range(joystick.get_numaxes()):
axis_value = joystick.get_axis(i)
print(f"軸 {i}: {axis_value:.2f}")
pygame.time.wait(100) # CPU負荷を抑える
pygame.quit()
▼venvノードの中にコードを書くことができます。
出力を順次受け取るので、Continuousを有効にしておきます。
▼実行すると、文字列として出力されました。
このままでは受信後のデータの扱いが難しいので、JSON形式で出力するようにしてもらいました。
import pygame
import json
import time
# 初期化
pygame.init()
pygame.joystick.init()
# 接続されているジョイスティックを取得
if pygame.joystick.get_count() == 0:
print("ジョイスティックが接続されていません")
exit()
joystick = pygame.joystick.Joystick(0)
joystick.init()
print(f"コントローラー: {joystick.get_name()} が接続されました")
# メインループ
running = True
while running:
pygame.event.pump() # イベントを更新
# 入力情報を格納する辞書
input_data = {
"buttons": {},
"axes": {},
"hats": {},
"timestamp": time.time() # タイムスタンプ追加
}
# ボタンの状態を取得
for i in range(joystick.get_numbuttons()):
input_data["buttons"][f"button_{i}"] = joystick.get_button(i)
# 軸(アナログスティック & トリガー)の状態を取得
for i in range(joystick.get_numaxes()):
input_data["axes"][f"axis_{i}"] = round(joystick.get_axis(i), 3) # 小数点3桁に丸める
# 十字キー(POV Hat)の状態を取得
for i in range(joystick.get_numhats()):
input_data["hats"][f"hat_{i}"] = joystick.get_hat(i)
# JSONとして出力
json_output = json.dumps(input_data, indent=4)
print(json_output)
pygame.time.wait(100) # CPU負荷を抑えるために 100ms スリープ
pygame.quit()
▼新しくノードを追加して実行してみました。
▼文字列のままですが、JSON形式になって出力されました。
ダッシュボードに値を表示する
このままでは入力の確認が難しいので、ダッシュボードに表示してみます。
まずはJSON形式にパースします。
▼jsonノードを入れました。
▼Objectになっていることが分かります。
msg.payloadの一つ下の階層のキーを指定して、値を取り出します。
▼textノードを追加しました。
先程デバッグウィンドウで確認していた値でアクセスするように設定します。
▼Value formatの中で、msg.payloadのbuttonsプロパティにアクセスしています。
他のプロパティも同様に設定します。ダッシュボード画面で確認してみました。
▼一つ下の階層の値を表示しただけなので、ものすごく長くなっています。
よく見るとhatsは全然変化せず、ボタンの名前は分かりにくかったので修正しました。
▼以下のように表示されるよう変更しました。
修正後のプログラムは以下です。コントローラを操作しながら確認していると、ボタンマッピングを間違えているところがあったので修正しました。
import pygame
import json
import time
# DualShock 4 のボタンマッピング
DS4_BUTTON_MAP = {
0: "cross", # ×ボタン
1: "circle", # ○ボタン
2: "square", # □ボタン
3: "triangle", # △ボタン
9: "L1",
10: "R1",
4: "share",
6: "options",
7: "L3",
8: "R3",
5: "ps", # PSボタン
15: "touchpad", # タッチパッドボタン
11: "dpad_up", # 十字キー 上
14: "dpad_right", # 十字キー 右
12: "dpad_down", # 十字キー 下
13: "dpad_left" # 十字キー 左
}
# DualShock 4 のスティック・トリガーのマッピング
DS4_AXIS_MAP = {
0: "left_stick_x",
1: "left_stick_y",
2: "right_stick_x",
3: "right_stick_y",
4: "L2_trigger",
5: "R2_trigger"
}
# 初期化
pygame.init()
pygame.joystick.init()
# ジョイスティックの取得
if pygame.joystick.get_count() == 0:
print("ジョイスティックが接続されていません")
exit()
joystick = pygame.joystick.Joystick(0)
joystick.init()
print(f"コントローラー: {joystick.get_name()} が接続されました")
# メインループ
running = True
while running:
pygame.event.pump() # イベント更新
# 入力データを格納
input_data = {
"buttons": {},
"axes": {}
}
# ボタンの状態取得
for i in range(joystick.get_numbuttons()):
button_name = DS4_BUTTON_MAP.get(i, f"button_{i}") # 未知のボタンは button_X 形式に
input_data["buttons"][button_name] = joystick.get_button(i)
# 軸(スティック & トリガー)状態取得
for i in range(joystick.get_numaxes()):
axis_name = DS4_AXIS_MAP.get(i, f"axis_{i}") # 未知の軸は axis_X 形式に
input_data["axes"][axis_name] = round(joystick.get_axis(i), 3)
# JSON出力
json_output = json.dumps(input_data, indent=4)
print(json_output)
pygame.time.wait(100) # CPU負荷を抑えるために 100ms スリープ
pygame.quit()
プロパティ名と値が対応していることは確認できたので、さらにそれぞれの値をダッシュボードに表示できるようにしました。
▼以下のフローを作成しました。
JSONノードでパースして、各プロパティの値をchangeノードでmsg.payloadに代入してから、textノードに渡しています。
textノードでプロパティにアクセスしてもいいのですが、msg.payloadに一旦代入しておくことで、他のノードと接続しやすくしています。
▼ダッシュボードでは以下のように表示されました。
入力に応じて値が変化することを確認できました。数値だけだと見づらいので、他のノードを使って見やすくしてみました。
最終的に以下のような形にまとめました。
▼-1~1で値が変化するものは、gaugeノードで表示するようにしました。
[{"id":"22052b56e60c6b59","type":"pip","z":"d6fad9e77ecc8414","venvconfig":"4657b6fbdbaf6f7e","name":"","arg":"pygame","action":"install","tail":false,"x":950,"y":1600,"wires":[["0c72442788c972a4"]]},{"id":"db0c6abf73dd7ddd","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":810,"y":1600,"wires":[["22052b56e60c6b59"]]},{"id":"0c72442788c972a4","type":"debug","z":"d6fad9e77ecc8414","name":"debug 372","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1110,"y":1600,"wires":[]},{"id":"0b5e3146dcca5a5d","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":810,"y":1660,"wires":[["24d547a46a258220"]]},{"id":"ca1bfe1d52949746","type":"debug","z":"d6fad9e77ecc8414","name":"debug 374","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1250,"y":1660,"wires":[]},{"id":"eff003e388bbf9ea","type":"json","z":"d6fad9e77ecc8414","name":"","property":"payload","action":"","pretty":false,"x":1090,"y":1660,"wires":[["ca1bfe1d52949746","491b7850367de634","b4597429d8641acb"]]},{"id":"51f6c7fef04fb561","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.axes.left_stick_x","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1780,"wires":[["39761b80535b3527"]]},{"id":"5b7f35ec662a5e20","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.axes.left_stick_y","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1820,"wires":[["3ce2279ab30c4ce3"]]},{"id":"8f4e6d55a7b038bb","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.axes.right_stick_x","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1860,"wires":[["d712775e6b8074bd"]]},{"id":"a10d0facc73ff82b","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.axes.right_stick_y","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1900,"wires":[["76d35c2ab7bfa619"]]},{"id":"3e63613dc47dd67e","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.axes.L2_trigger","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1940,"wires":[["81258300e2a180ca"]]},{"id":"c46e64b54823dad9","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.axes.R2_trigger","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":1980,"wires":[["8a00aa6ab2b170be"]]},{"id":"24d547a46a258220","type":"venv","z":"d6fad9e77ecc8414","venvconfig":"4657b6fbdbaf6f7e","name":"","code":"import pygame\nimport json\nimport time\n\n# DualShock 4 のボタンマッピング\nDS4_BUTTON_MAP = {\n 0: \"cross\", # ×ボタン\n 1: \"circle\", # ○ボタン\n 2: \"square\", # □ボタン\n 3: \"triangle\", # △ボタン\n 9: \"L1\",\n 10: \"R1\",\n 4: \"share\",\n 6: \"options\",\n 7: \"L3\",\n 8: \"R3\",\n 5: \"ps\", # PSボタン\n 15: \"touchpad\", # タッチパッドボタン\n 11: \"dpad_up\", # 十字キー 上\n 14: \"dpad_right\", # 十字キー 右\n 12: \"dpad_down\", # 十字キー 下\n 13: \"dpad_left\" # 十字キー 左\n}\n\n# DualShock 4 のスティック・トリガーのマッピング\nDS4_AXIS_MAP = {\n 0: \"left_stick_x\",\n 1: \"left_stick_y\",\n 2: \"right_stick_x\",\n 3: \"right_stick_y\",\n 4: \"L2_trigger\",\n 5: \"R2_trigger\"\n}\n\n# 初期化\npygame.init()\npygame.joystick.init()\n\n# ジョイスティックの取得\nif pygame.joystick.get_count() == 0:\n print(\"ジョイスティックが接続されていません\")\n exit()\n\njoystick = pygame.joystick.Joystick(0)\njoystick.init()\nprint(f\"コントローラー: {joystick.get_name()} が接続されました\")\n\n# メインループ\nrunning = True\nwhile running:\n pygame.event.pump() # イベント更新\n\n # 入力データを格納\n input_data = {\n \"buttons\": {},\n \"axes\": {}\n }\n\n # ボタンの状態取得\n for i in range(joystick.get_numbuttons()):\n button_name = DS4_BUTTON_MAP.get(i, f\"button_{i}\") # 未知のボタンは button_X 形式に\n input_data[\"buttons\"][button_name] = joystick.get_button(i)\n\n # 軸(スティック & トリガー)状態取得\n for i in range(joystick.get_numaxes()):\n axis_name = DS4_AXIS_MAP.get(i, f\"axis_{i}\") # 未知の軸は axis_X 形式に\n input_data[\"axes\"][axis_name] = round(joystick.get_axis(i), 3)\n\n # JSON出力\n json_output = json.dumps(input_data, indent=4)\n print(json_output)\n\n pygame.time.wait(100) # CPU負荷を抑えるために 100ms スリープ\n\npygame.quit()\n","continuous":true,"x":950,"y":1660,"wires":[["eff003e388bbf9ea"]]},{"id":"4f06a6b1dc5d680a","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.cross","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1600,"wires":[["d6ec5892bc127fe9"]]},{"id":"d6ec5892bc127fe9","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Cross","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":1600,"wires":[]},{"id":"1a6380332cf5cd52","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.circle","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1640,"wires":[["9cc87a9c3e6f0035"]]},{"id":"9cc87a9c3e6f0035","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Circle","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":1640,"wires":[]},{"id":"b44aa7c1d66b79b8","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.square","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1680,"wires":[["1f57f89452d3afe7"]]},{"id":"1f57f89452d3afe7","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Square","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1740,"y":1680,"wires":[]},{"id":"0f04b5cd85d1395e","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.triangle","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1720,"wires":[["46f52d05fa3c247a"]]},{"id":"46f52d05fa3c247a","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Triangle","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1740,"y":1720,"wires":[]},{"id":"91077cd2bba8529e","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.ps","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1760,"wires":[["1a75d59c7a27052a"]]},{"id":"1a75d59c7a27052a","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":0,"height":0,"name":"","label":"PS","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":1760,"wires":[]},{"id":"2e60f14fbe23c3ce","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.share","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1800,"wires":[["aa67e9490b49afb8"]]},{"id":"aa67e9490b49afb8","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":0,"height":0,"name":"","label":"Share","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":1800,"wires":[]},{"id":"c662c206f9937b74","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.options","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1840,"wires":[["a54da400d2fea125"]]},{"id":"a54da400d2fea125","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":0,"height":0,"name":"","label":"Options","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1740,"y":1840,"wires":[]},{"id":"31e109f6ff5877da","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.L3","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1880,"wires":[["0c3040e3a045dde5"]]},{"id":"0c3040e3a045dde5","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"L3","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":1880,"wires":[]},{"id":"98b3863756dff0d4","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.R3","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1920,"wires":[["75885030a7baedc9"]]},{"id":"75885030a7baedc9","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"R3","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":1920,"wires":[]},{"id":"330ecca51674ad7b","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.L1","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":1960,"wires":[["2c5e7d6378b5313d"]]},{"id":"2c5e7d6378b5313d","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"L1","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":1960,"wires":[]},{"id":"9f81bcc020d2d0aa","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.R1","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":2000,"wires":[["c0e5db9534442249"]]},{"id":"c0e5db9534442249","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"R1","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":2000,"wires":[]},{"id":"7332be9e450c6c51","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.dpad_up","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":2040,"wires":[["ac98c870a5f214d2"]]},{"id":"ac98c870a5f214d2","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Up","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":2040,"wires":[]},{"id":"439b65268ed931df","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.dpad_down","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":2080,"wires":[["066bb1ab3979c7ae"]]},{"id":"066bb1ab3979c7ae","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Down","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":2080,"wires":[]},{"id":"3c27694e8cdd4289","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.dpad_left","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":2120,"wires":[["4dd05ee6ec60fadb"]]},{"id":"4dd05ee6ec60fadb","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Left","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":2120,"wires":[]},{"id":"86d8287dcc58e2a2","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.dpad_right","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":2160,"wires":[["91fd7eba418305a4"]]},{"id":"91fd7eba418305a4","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":"3","height":"1","name":"","label":"Right","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":2160,"wires":[]},{"id":"a6dcfadb9bf8838c","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.buttons.touchpad","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":2200,"wires":[["c5f5d5bb1ed13491"]]},{"id":"c5f5d5bb1ed13491","type":"ui_text","z":"d6fad9e77ecc8414","group":"227d3ca77163ce82","order":0,"width":0,"height":0,"name":"","label":"TouchPad","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1740,"y":2200,"wires":[]},{"id":"39761b80535b3527","type":"ui_gauge","z":"d6fad9e77ecc8414","name":"","group":"62a28eb47ad4b20e","order":6,"width":0,"height":0,"gtype":"compass","title":"Left X","label":"","format":"{{value}}","min":"1","max":"-1","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":1170,"y":1780,"wires":[]},{"id":"3ce2279ab30c4ce3","type":"ui_gauge","z":"d6fad9e77ecc8414","name":"","group":"62a28eb47ad4b20e","order":6,"width":0,"height":0,"gtype":"compass","title":"Left Y","label":"","format":"{{value}}","min":"-1","max":"1","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":1170,"y":1820,"wires":[]},{"id":"d712775e6b8074bd","type":"ui_gauge","z":"d6fad9e77ecc8414","name":"","group":"6e28d9e8ae4ff93d","order":6,"width":0,"height":0,"gtype":"compass","title":"Right X","label":"","format":"{{value}}","min":"1","max":"-1","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":1180,"y":1860,"wires":[]},{"id":"76d35c2ab7bfa619","type":"ui_gauge","z":"d6fad9e77ecc8414","name":"","group":"6e28d9e8ae4ff93d","order":6,"width":0,"height":0,"gtype":"compass","title":"Right Y","label":"","format":"{{value}}","min":"-1","max":"1","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":1180,"y":1900,"wires":[]},{"id":"81258300e2a180ca","type":"ui_gauge","z":"d6fad9e77ecc8414","name":"","group":"62a28eb47ad4b20e","order":6,"width":0,"height":0,"gtype":"gage","title":"L2","label":"","format":"{{value}}","min":"-1","max":"1","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":1170,"y":1940,"wires":[]},{"id":"8a00aa6ab2b170be","type":"ui_gauge","z":"d6fad9e77ecc8414","name":"","group":"6e28d9e8ae4ff93d","order":6,"width":0,"height":0,"gtype":"gage","title":"R2","label":"","format":"{{value}}","min":"-1","max":"1","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":1170,"y":1980,"wires":[]},{"id":"491b7850367de634","type":"junction","z":"d6fad9e77ecc8414","x":780,"y":1880,"wires":[["51f6c7fef04fb561","8f4e6d55a7b038bb","5b7f35ec662a5e20","a10d0facc73ff82b","3e63613dc47dd67e","c46e64b54823dad9"]]},{"id":"b4597429d8641acb","type":"junction","z":"d6fad9e77ecc8414","x":1340,"y":1880,"wires":[["4f06a6b1dc5d680a","1a6380332cf5cd52","b44aa7c1d66b79b8","0f04b5cd85d1395e","91077cd2bba8529e","2e60f14fbe23c3ce","c662c206f9937b74","31e109f6ff5877da","98b3863756dff0d4","330ecca51674ad7b","9f81bcc020d2d0aa","7332be9e450c6c51","439b65268ed931df","3c27694e8cdd4289","86d8287dcc58e2a2","a6dcfadb9bf8838c"]]},{"id":"4657b6fbdbaf6f7e","type":"venv-config","venvname":"pyenv","version":"default"},{"id":"227d3ca77163ce82","type":"ui_group","name":"Buttons","tab":"f6642a80f4f5605a","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"62a28eb47ad4b20e","type":"ui_group","name":"Left","tab":"f6642a80f4f5605a","order":3,"disp":true,"width":"6","collapse":false,"className":""},{"id":"6e28d9e8ae4ff93d","type":"ui_group","name":"Right","tab":"f6642a80f4f5605a","order":4,"disp":true,"width":"6","collapse":false,"className":""},{"id":"f6642a80f4f5605a","type":"ui_tab","name":"Controller","icon":"dashboard","disabled":false,"hidden":false}]
▼以下のように表示されました。
実際にコントローラを操作しながら確認してみました。
▼プログラムで100msのスリープが入っていましたが、問題なく動作していました。
▼ボタンも数値ではない表示にしたいところですね。
通信速度が重要な場面だと、スリープを調整する必要がありそうです。
最後に
Node-REDでDUALSHOCK 4の入力を受け付けることができるようになりました。さらに他のソフトウェアと通信することで、いろいろ使えそうです。
ESP32はDUALSHOCK 4と同じBluetooth Classicに対応していますが、ESP32C3はBLEのみでBluetooth Classicに対応していないという問題がありました。Node-REDで中継することで、ESP32C3でもDUALSHOCK 4と通信することができそうだなと考えています。
▼以下の記事で調べたことがあります。
また、ノードとしてDUALSHOCK 4と通信できるようにすると、便利に使えそうだなと思っています。ノードなら出力を複数のピンで指定することができるので、今回のようにchangeノードを大量に配置しないで済みます。