はじめに
家に使っていないSurface Go 2(2in1 PC)があります。なにか使い道がないかどうか考えていたらいい案が思いつきました。きっかけは「Elgato Stream Deck」という商品の紹介記事を読んだことでした。「Elgato Stream Deck」が流行っているらしいです。そこで、Surface Go 2をStream Deckのようなマクロパッドにしようと考えました。Surface Go 2とArduino Uno R3とArduino Leonardoを使います。幸いにもArduino Uno R3の互換ボードとArduino Leonardoも家にありました。イメージ図
こんな感じで接続しています。Surface GO 2上のボタンを押すことでメインPCを操作することが可能です。
シリアル通信は1バイトのデータを送信するのみとします。これでも0~255の256通りの設定が可能です。
完成図
二つのArduinoを接続しているのはGND同士の1本とシリアル通信用の2本、計3本のみです。
Unoの8番ピンとLeonardoの9番ピン
Unoの9番ピンとLeonardoの8番ピン
UnoのGNDとLeonardoのGND
Arduino間でソフトウェアシリアルを使う場合はGND同士をつなげ!!
これに気付かないで2日間を無駄にしてしまいました。
実際活用中の写真
特徴
メインPCにUSB接続のキーボードを接続している感じです。そのためメインPC側にはアプリなどのインストールを含め何の設定も必要ありません。2in1 PC(またはタブレット)とArduino2枚を携帯すれば携帯可能なマクロパッドになります。小型のボードを使うと全体の小型化は簡単だと思います。WiFiやBluetooth搭載のボードを使えば無線化もできると思います。メインPCのみで同様のことをしようとしても作成したアプリのボタンを押そうとすると使用中のアプリ(たとえばパワーポイントなど)が非アクティブ化されてしまうので不可能です。今回のシステムはその点を解決できます。欠点
Arduinoプログラム以外にもう一つプログラム言語を必要とします。今回はPythonを使用していますがなんでも構いません。プログラム
Arduinoスケッチ
Unoのスケッチ
#include <SoftwareSerial.h> SoftwareSerial mySerial(8,9); void setup() { Serial.begin(9600); mySerial.begin(2400); } void loop() { if(Serial.available()>0){ int var = Serial.read(); mySerial.write(var); } }
Leonardoのスケッチ
ボタンが増えるたびにこのスケッチに設定を書き足す必要があります。#include <SoftwareSerial.h> #include <Keyboard.h> SoftwareSerial mySerial(8,9); void setup() { mySerial.begin(2400); } void loop() { // put your main code here, to run repeatedly: if (mySerial.available()>0){ int var = mySerial.read(); switch(var){ //PowerPoint settings case 0: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('n'); Keyboard.write('x'); Keyboard.write('h'); break; case 1: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('n'); Keyboard.write('p'); Keyboard.write('d'); break; case 2: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('n'); Keyboard.write('s'); Keyboard.write('h'); break; case 3: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('t'); break; case 4: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('l'); break; case 5: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('r'); break; case 6: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('b'); break; case 7: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('c'); break; case 8: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('m'); break; case 9: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('h'); break; case 10: Keyboard.write(KEY_LEFT_ALT); delay(100); Keyboard.write('h'); Keyboard.write('g'); Keyboard.write('a'); Keyboard.write('v'); break; //YouTube settings case 20: Keyboard.write('j'); break; case 21: Keyboard.write('l'); break; case 22: Keyboard.write('k'); break; case 23: Keyboard.write('f'); break; } } }
Pythonスクリプト
GUIはPySide6を使っています。メインスクリプトとそれぞれアプリごとのファイルが存在します。
メインファイル(main.py)
from PySide6 import QtSerialPort from PySide6.QtCore import Qt, QSize, Slot, QIODevice from PySide6.QtWidgets import QApplication, QMainWindow, QToolBar, QWidget, QToolButton, QStatusBar, QStackedWidget from PySide6.QtGui import QIcon from youtube import YoutubeWidge from powerpoint import PowerpointWidget toolbar_items = [ { 'name': 'Menu', 'icon': './icon/menu.svg', 'widget': QWidget}, { 'name': 'PowerPoint', 'icon': './icon/powerpoint.svg', 'widget': PowerpointWidget}, { 'name': 'YouTube', 'icon': './icon/youtube.svg', 'widget': YoutubeWidge}, ] class Window(QMainWindow): def __init__(self): super().__init__() self.serial = QtSerialPort.QSerialPort( 'COM3', baudRate=QtSerialPort.QSerialPort.BaudRate.Baud9600) self.serial.open(QIODevice.OpenModeFlag.WriteOnly) self.openMenu = False self.setFixedSize(QSize(900, 600)) self.stackedWidget = QStackedWidget() self.toolbar = QToolBar() self.toolbar.setIconSize(QSize(36,36)) self.toolbar.setStyleSheet("QToolBar {background: rgb(51, 51, 51); spacing: 10px;}") self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar) self.toolbar.setMovable(False) self.toolbuttons = [] self.widgetlist = [] for i, each_item in enumerate(toolbar_items): self.toolbuttons.append(QToolButton()) self.toolbuttons[-1].setText(each_item.get('name')) self.toolbuttons[-1].setIcon(QIcon(each_item.get('icon'))) self.toolbuttons[-1].Name = i self.toolbuttons[-1].setStyleSheet("QToolButton {color: white; font: 16px; font-weight: bold}") self.toolbuttons[-1].clicked.connect(self.pushToolButton) self.toolbar.addWidget(self.toolbuttons[-1]) self.widgetlist.append(each_item.get('widget')()) for i, eachwidget in enumerate(self.widgetlist): self.stackedWidget.addWidget(eachwidget) if i > 0: eachwidget.push_button_signal.connect(self.send_serial) self.statusbar = QStatusBar() self.setStatusBar(self.statusbar) self.setCentralWidget(self.stackedWidget) def pushToolButton(self): match int(self.sender().Name): case 0: self.openMenu = not self.openMenu if self.openMenu == True: for each_toolbutton in self.toolbuttons: each_toolbutton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) else: for each_toolbutton in self.toolbuttons: each_toolbutton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) case _: self.statusbar.showMessage(self.sender().text()) self.stackedWidget.setCurrentIndex(self.sender().Name) def closeEvent(self, e): self.serial.close() e.accept() @Slot(int) def send_serial(self, x): self.serial.write(x.to_bytes(1, 'big')) if __name__ == "__main__": app = QApplication([]) ex =Window() ex.show() app.exec()
PowerPointのためのファイル(powerpoint.py)
from PySide6.QtCore import Signal from PySide6.QtWidgets import QPushButton, QVBoxLayout, QHBoxLayout, QGridLayout, QWidget, QSizePolicy from PySide6.QtGui import QIcon class PowerpointWidget(QWidget): push_button_signal = Signal(int) def __init__(self) -> None: super().__init__() self.button0 = QPushButton('テキスト挿入') self.button0.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button0.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button0.setIcon(QIcon('./icon_black/edit.svg')) self.button0.clicked.connect(lambda :self.button_push(0)) self.button1 = QPushButton('画像挿入') self.button1.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button1.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button1.setIcon(QIcon('./icon_black/image.svg')) self.button1.clicked.connect(lambda :self.button_push(1)) self.button2 = QPushButton('図形挿入') self.button2.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button2.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button2.setIcon(QIcon('./icon_black/insertfig.svg')) self.button2.clicked.connect(lambda :self.button_push(2)) self.button3 = QPushButton('上揃え') self.button3.setStyleSheet('font: 20px; font-weight: bold') self.button3.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button3.clicked.connect(lambda :self.button_push(3)) self.button4 = QPushButton('左揃え') self.button4.setStyleSheet('font: 20px; font-weight: bold') self.button4.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button4.clicked.connect(lambda :self.button_push(4)) self.button5 = QPushButton('右揃え') self.button5.setStyleSheet('font: 20px; font-weight: bold') self.button5.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button5.clicked.connect(lambda :self.button_push(5)) self.button6 = QPushButton('下揃え') self.button6.setStyleSheet('font: 20px; font-weight: bold') self.button6.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button6.clicked.connect(lambda :self.button_push(6)) self.button7 = QPushButton('左右中央揃え') self.button7.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button7.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button7.setIcon(QIcon('./icon_black/centeralignment_v.svg')) self.button7.clicked.connect(lambda :self.button_push(7)) self.button8 = QPushButton('上下中央揃え') self.button8.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button8.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button8.setIcon(QIcon('./icon_black/centeralignment_h.svg')) self.button8.clicked.connect(lambda :self.button_push(8)) self.button9 = QPushButton('左右等間隔に整列') self.button9.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button9.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button9.setIcon(QIcon('./icon_black/equal_h.svg')) self.button9.clicked.connect(lambda :self.button_push(9)) self.button10 = QPushButton('上下等間隔に整列') self.button10.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button10.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button10.setIcon(QIcon('./icon_black/equal_v.svg')) self.button10.clicked.connect(lambda :self.button_push(10)) mainlayout = QVBoxLayout() upperlayout = QHBoxLayout() upperleftlayout = QGridLayout() upperleftlayout.addWidget(self.button3, 0, 1) upperleftlayout.addWidget(self.button4, 1, 0) upperleftlayout.addWidget(self.button5, 1, 2) upperleftlayout.addWidget(self.button6, 2, 1) upperrightlayout = QVBoxLayout() upperrightlayout.addWidget(self.button7) upperrightlayout.addWidget(self.button8) upperrightlayout.addWidget(self.button9) upperrightlayout.addWidget(self.button10) upperlayout.addLayout(upperleftlayout) upperlayout.addLayout(upperrightlayout) lowerlayout = QHBoxLayout() lowerlayout.addWidget(self.button0) lowerlayout.addWidget(self.button1) lowerlayout.addWidget(self.button2) mainlayout.addLayout(upperlayout, 7) mainlayout.addLayout(lowerlayout, 3) self.setLayout(mainlayout) def button_push(self, x: int) -> None: self.push_button_signal.emit(x)
YouTubeのためのファイル(youtube.py)
from PySide6.QtCore import Signal from PySide6.QtWidgets import QGridLayout, QSizePolicy, QPushButton, QWidget from PySide6.QtGui import QIcon class YoutubeWidge(QWidget): push_button_signal = Signal(int) def __init__(self) -> None: super().__init__() self.button1 = QPushButton('10秒もどる') self.button1.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button1.setIcon(QIcon('./icon_black/chevrons-left')) self.button1.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button1.clicked.connect(lambda :self.button_push(20)) self.button2 = QPushButton('10秒すすむ') self.button2.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button2.setIcon(QIcon('./icon_black/chevrons-right')) self.button2.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button2.clicked.connect(lambda :self.button_push(21)) self.button3 = QPushButton('再生/停止') self.button3.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button3.setIcon(QIcon('./icon_black/play.svg')) self.button3.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button3.clicked.connect(lambda :self.button_push(22)) self.button4 = QPushButton('フルスクリーン') self.button4.setStyleSheet('font: 20px; font-weight: bold; qproperty-iconSize: 32px;') self.button4.setIcon(QIcon('./icon_black/monitor.svg')) self.button4.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.button4.clicked.connect(lambda :self.button_push(23)) layout = QGridLayout() layout.addWidget(self.button1, 0, 0) layout.addWidget(self.button2, 0, 2) layout.addWidget(self.button3, 0, 1) layout.addWidget(self.button4, 1, 1) self.setLayout(layout) def button_push(self, x: int) -> None: self.push_button_signal.emit(x)
その他
自作したアイコンもありますが、ほとんどはこちらのものを使用させて頂きました。feathericons.com
関連記事
物理的なキーを持つマクロパッドについてはこちらで記事にしています。touch-sp.hatenablog.com
touch-sp.hatenablog.com
Pyside6でツールバーを作る方法はこちらで記事にしています。
touch-sp.hatenablog.com
Pythonスクリプト改良
新しい記事を書きました。touch-sp.hatenablog.com
システムの小型化
新しい記事を書きました。touch-sp.hatenablog.com
新しいページの追加
新しい記事を書きました。touch-sp.hatenablog.com