M5StickC でキーボード操作を自動化する

某ゲームでアイテムを交換する作業が面倒になってきたので キーボード操作を自動化したいと思った。

まず思いついたのは、手元にあった サンワサプライ製のプログラマブルテンキー NT-19UH2BK。 久しぶりに引っぱりだして確認したところ、これは Ctrl + C のような同時押しには対応しているが、 ウェイトをはさみながら複数のキーを順次入力する用途には対応していなかった。

次に AZ-Macro というマクロ機能のある自作 Bluetooth キーボードキットがあることを思い出した。 これは ESP32 で作られていて、 ファームウェアのソースコードも公開 されている。 手持ちの M5StickC なら同じ ESP32 が内蔵されており Bluetooth にも対応しているため、目的を実現できるのではないか?

ということで、M5StickC を Bluetooth キーボードとして PC へ接続し操作を自動化する。

Arduino IDE へ ESP32-BLE-Keyboard をインストール

まずは M5StickC を Bluetooth キーボードとして使うためのライブラリを Arduino IDE へインストールする。

  1. Releases · T-vK/ESP32-BLE-Keyboard から ESP32-BLE-Keyboard.zip をダウンロード
  2. Arduino IDE のメニューで [スケッチ] - [ライブラリをインクルード] - [.ZIP形式のライブラリをインストール…] をクリック
  3. さきほどダウンロードした ESP32-BLE-Keyboard.zip を選択

ESP32-BLE-Keyboard の使い方

公式のサンプル を見れば大まかな使い方はわかると思うが、メソッドの説明がないので補足しておく。

BleKeyboard クラスの以下のメソッドでキーを操作できる。

  • print: 文字列を入力
  • write: キーを押してすぐに離す
  • press: キーを押す (release するまで押したままにする)
  • release: キーを離す
  • releaseAll: すべてのキーを離す

キーの指定は、英数記号の場合、文字をそのまま渡す。 大文字を渡すと自動でシフトキーが補完され大文字での入力になる。

bleKeyboard.write('a');

ファンクションキー、修飾キーなどの場合は BleKeyboard.h で定義されている定数を渡す。

スケッチの作成

M5StickC のボタンA (正面の「M5」ボタン) を押すと 500ms 間隔で順番に B, L, E, Enterキーを押すサンプルスケッチを作成した。 今回は同時押しが不要だったため keystroke に書かれたキーを順に write で出力するだけとなっている。

動作確認

以下の手順で動作を確認できる。

  1. M5StickC にスケッチを書き込む
  2. M5StickC の画面に “Waiting 5 seconds…” と表示されていることを確認
  3. PC で Bluetooth デバイスを検索し “ESP32 BLE Keyboard” へ接続
  4. メモ帳などのアプリを開く
  5. M5StickC のボタンA (正面の「M5」ボタン) を押す

これで複雑なキーボードマクロでも思い通りに実行できるようになった。 キーボードでできることなら何でもボタン一発で実行できるため、 思っていた以上に利便性が高い。

参考サイト

M5StickC で毎朝照明を自動で点灯する

最近寒いのでカーテンを遮光・遮熱のものに変えた。 遮熱のほうはあまり実感がないが遮光のほうはばっちりで、 昼間でもカーテンを閉めれば部屋がかなり暗くなる。

寝てる間は暗くてよいのだが、 朝になっても明るさで自然に目が覚めるということがなくなった。 時計を見ないと大まかな時間すら分からないのは思いの外不便だ。 そこで、以前購入した M5StickC を使って 毎朝自動で照明を点灯させることを思いついた。

使用する照明器具とリモコン

私の部屋の照明はNECライティング(現ホタルクス)の LEDシーリングライトHLDX0801 を使用している。 昨年買い換えたのだが、 これはシーリングライトとしてはかなりの薄型で、 引掛シーリングと合わせても高さ6cmくらいしかない。 以前使用していたものは高さ15cmほどあったため、 圧迫感がなくなり気に入っている。 唯一の不満点はリモコンが付属していないことで、別途 RE0206 を購入し使用している。

リモコンの信号解析

まずは点灯時に RE0206 から発信される赤外線信号を調べるため Grove 接続の IR ユニット を購入した。 M5StickC 単体でも赤外線の発信はできるが、受信はできないため、 リモコンの信号を調べる場合は別途購入する必要がある。

M5StickC/M5Stack Fireで赤外線リモコンを作ろうと四苦八苦したのでメモ - カワリモノ息子の技術メモ的な〜 を参考に Arduino IDE から IRremoteESP8266 をインストールし、スケッチ例の IRvecvDumpV3 を開く。 kRecvPin を 33 に変更して M5StickC へ書き込んだあと、 IR ユニットへ向けてリモコンのボタンを押すと シリアルモニタへ信号が表示された。

赤外線リモコンの信号にはいくつか種類があるらしいが、 NEC製品なので当然NECフォーマットだった。

スケッチの作成

M5StickC で設定時刻に赤外線信号を送信するスケッチを作成した。

saasan/m5stickc-ir-timer: M5StickCで設定時刻に赤外線信号を送信する

#include <M5StickC.h>
#include <WiFi.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include "wifi-ssid.h"

// -----------------------------------------------------------------------------
// 定数
// -----------------------------------------------------------------------------
// 赤外線LEDのピン番号
// M5StickC内蔵の赤外線LEDを使用する場合は9
// Grove接続のIRユニットを使用する場合は32
const uint8_t IR_SEND_PIN = 9;
// 赤外線送信するデータ
const uint64_t IR_SEND_DATA = 0x41B6659A;
// GMTからの時間差(秒)
const long JST = 9 * 60 * 60;
// NTPサーバ
const char *NTP_SERVER = "ntp.nict.jp";
// 電源ボタンが1秒未満押された
const uint8_t AXP_WAS_PRESSED = 2;
// ボタンが長押しされたと判定する時間(ms)
const uint32_t BUTTON_PRESSED_MS = 500;
// 最後にボタンを押してから画面を省電力にするまでの時間(ms)
const unsigned long SCREEN_OFF_MS = 3000;
// 通常時の画面輝度
const uint8_t SCREEN_ON_BRIGHTNESS = 12;
// 省電力時の画面輝度
const uint8_t SCREEN_OFF_BRIGHTNESS = 8;

// -----------------------------------------------------------------------------
// 変数
// -----------------------------------------------------------------------------
// IRremoteESP8266のIRsendクラス
IRsend irsend(IR_SEND_PIN);
// 現在時刻
struct tm now;
// 赤外線送信する時刻の時間
int timer_hour = 7;
// 赤外線送信する時刻の分
int timer_min = 0;
// 赤外線送信済みならtrue
bool ir_sent = false;
// 最後にボタンが押された時間
unsigned long button_pressed_millis = 0;

// -----------------------------------------------------------------------------
// 関数
// -----------------------------------------------------------------------------
// 現在時刻を表示
void showCurrentTime() {
    char message[50];

    sprintf(
        message,
        " now:\n  %04d/%02d/%02d %02d:%02d:%02d\n timer:\n  %02d:%02d",
        now.tm_year + 1900,
        now.tm_mon + 1,
        now.tm_mday,
        now.tm_hour,
        now.tm_min,
        now.tm_sec,
        timer_hour,
        timer_min);

    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print(message);
}

// 無線LAN接続
void connectWiFi(const char *ssid, const char *passphrase) {
    M5.Lcd.printf("Connecting to %s", ssid);
    WiFi.begin(ssid, passphrase);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        M5.Lcd.print(".");
    }
    M5.Lcd.print("\nconnected!");
    delay(500);
    M5.Lcd.fillScreen(BLACK);
}

// 画面の輝度を上げる
void screenOn() {
    M5.Axp.ScreenBreath(SCREEN_ON_BRIGHTNESS);
    button_pressed_millis = millis();
}

// 画面の輝度を下げる
void screenOff() {
    M5.Axp.ScreenBreath(SCREEN_OFF_BRIGHTNESS);
}

void setup() {
    M5.begin();

    // 画面の表示設定
    M5.Lcd.setRotation(1);
    M5.Lcd.setTextFont(2);
    // ピンモードの設定
    pinMode(IR_SEND_PIN, OUTPUT);
    // 無線LANへ接続
    connectWiFi(WIFI_SSID, WIFI_PASSPHRASE);
    // NTPの設定
    configTime(JST, 0, NTP_SERVER);

    button_pressed_millis = millis();
}

void loop() {
    M5.update();

    // ボタンAが押されたらアラーム時刻の時間を変更
    if (M5.BtnA.wasPressed() || M5.BtnA.pressedFor(BUTTON_PRESSED_MS)) {
        timer_hour++;
        if (timer_hour > 23) timer_hour = 0;

        screenOn();
    }
    // ボタンBが押されたらアラーム時刻の分を変更
    if (M5.BtnB.wasPressed() || M5.BtnB.pressedFor(BUTTON_PRESSED_MS)) {
        timer_min++;
        if (timer_min > 59) timer_min = 0;

        screenOn();
    }
    // 電源ボタンが押されたらリセット
    if (M5.Axp.GetBtnPress() == AXP_WAS_PRESSED) {
        esp_restart();
    }

    // 現在時刻を取得
    getLocalTime(&now);
    showCurrentTime();

    // 最後にボタンを押してから時間が経過している場合は画面を省電力化
    if ((millis() - button_pressed_millis) > SCREEN_OFF_MS) {
        screenOff();
    }

    if (now.tm_hour == timer_hour && now.tm_min == timer_min && now.tm_sec == 0) {
        // 赤外線送信していなければ電源ON信号を送信
        if (!ir_sent) {
            irsend.sendNEC(IR_SEND_DATA);
            ir_sent = true;
        }
    } else {
        ir_sent = false;
    }

    delay(100);
}

IR_SEND_DATA の 0x41B6659A が上記で解析した点灯するための信号で、 これを IRsend クラスの sendNEC メソッドで送信している。

タイマーの時刻は ボタンA(正面の「M5」ボタン)で時、 ボタンB(側面の小さいボタン)で分を変更できるようにした。

送信するリモコンの信号を変えればいろいろと応用できそう。

消してしまった WSL 環境のバックアップからファイルを取り出す

PC を新しいものへ移行する際に WSL 環境の中身を移行するのを忘れていた。 というか、別の環境に最新のファイルがあると思い込んでいたので敢えて移行しなかったのだ。 実際は自分の旧 PC の WSL 環境にあったものが最新のファイルで、 それに気付いたときにはディスクの初期化が済んでいた。

旧 PC の Ubuntu 内には作業中のファイルがあり、 これが消えてしまうと10時間ほどかけた作業が無駄になってしまう。 こんなこともあろうかと、旧 PC のディスクを Disk2vhd で VHDX ファイル化したものは残しておいたので、 このバックアップファイルから必要なファイルを取り出すことにした。

旧 PC のバックアップから Ubuntu のディスクイメージを探す

まずは旧 PC のバックアップから Ubuntu のディスクイメージを探す。 VHDX ファイルをダブルクリックすると自動でマウントされるので その中を探したところ以下のパスにディスクイメージがあった。

\Users\ユーザー名\AppData\Local\Packages\CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc\LocalState\ext4.vhdx

このファイルをマウントして中身を取り出して完了かと思ったら、 ファル名通りファイルシステムが ext4 だからなのか Windows ではマウントできなかった。

しかたないので新 PC の WSL へ新しいディストリビューションを入れて ext4.vhdx を置き換えることにする。

2022/08/27 追記

2021/12/26 にリリースされた 21.07 から 7-Zip が VHDX ファイルへ対応しています。 ファイルシステムが ext4 でも 7-Zip で開いてファイルを取り出せることを確認済みです。

Alpine WSL をインストールする

ext4.vhdx からファイルを取り出すため Alpine WSL をインストールした。 これはおそらく新 PC で使ってないディストリビューションならなんでもいい。 新 PC でも既に Ubuntu を使い始めていたのでそれ以外で、 インストール作業に時間をかけたくないこともあり 軽量なことで有名な Alpine Linux を選択した。

インストール後、念のため一度起動して初期設定を行っておく。

WSL のサービスを停止して ext4.vhdx を置き換える

WSL のサービスが起動したままだとファイルがロックされていて置き換えられない。 コマンドプロンプトか PowerShell を管理者権限で起動し、 以下のコマンドで WSL のサービスを止める。

net stop LxssManager

次に以下のパスにある Alpine WSL の ext4.vhdx を旧 PC のバックアップから取り出したもので置き換える。

\Users\ユーザー名\AppData\Local\Packages\36828agowa338.AlpineWSL_my43bytk1c4nr\LocalState\ext4.vhdx

最後に以下のコマンドで WSL のサービスを起動する。

net start LxssManager

必要なファイルを取り出す

あとは alpine コマンドで起動し旧 PC の Ubuntu 環境へ入ってファイルを取り出すことができた。

最近知った Bash の便利機能

cd -

一つ前のディレクトリへ戻る。

pushd / popd で2つのディレクトリを往復することがあったけどこっちのほうが速い。

Ctrl + u, Ctrl + y

Ctrl + u でカーソル位置から行頭まで切り取る。 Ctrl + y で切り取ったものを貼り付ける。

git commit -m "hoge" とか書いたところで git add してないことに気付いたりとか、 コマンド入力中に別のコマンドを先に実行したくなったときに便利。

ちなみに Ctrl + k ならカーソル位置から行末まで切り取れる。

スタートメニューから消えた Dell Command | Update を復活させる

Dell Command | Update を実行したあとにスタートメニューから Dell Command | Update が消えることがある。

しばらく待っていれば自動で復活することもあれば復活しないこともある。 復活しないときは一度アンインストールして Dell のサイトからダウンロード したものを入れ直していたが、けっこう手間がかかって面倒だった。 そもそもアプリの一覧に残ってるならもっと簡単に復活できるんじゃないか?

そう思ってスタートメニューだけ復活させる方法がないか調べたら以下の方法で復活できた。

  1. 管理者権限で PowerShell を起動する。
  2. 以下のコマンドを実行する。

    $ManifestPath = (Get-AppxPackage -AllUsers -Name "DellInc.DellCommandUpdate").InstallLocation + "\Appxmanifest.xml"
    Add-AppxPackage -Path $ManifestPath -Register -DisableDevelopmentMode
    

※Get-AppxPackage に -AllUsers オプションを付けて実行するのに管理者権限が必要。

参考 : Add-AppxPackage (AppX) | Microsoft Docs