【仕事効率化】使っていない 2in1 PC(またはタブレット)を有効活用、マクロパッドとして復活させる




はじめに

家に使っていない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